diff --git a/pingora_tutorial/ctx.md b/pingora_tutorial/ctx.md new file mode 100644 index 000000000..ac15cc636 --- /dev/null +++ b/pingora_tutorial/ctx.md @@ -0,0 +1,325 @@ +# Tutorial: Distinguishing Between Beta and Regular Users with a Custom Pingora Proxy + +This example showcases how to **maintain user-specific context** in a Pingora-based proxy. Specifically, we’ll show you how to: + +- Track whether a request comes from a **beta user** (via a header, e.g. `beta-flag`). +- Use separate **counters**: + - A global counter tracking all requests. + - A beta user counter tracking only beta users. + +The proxy then routes beta users to **`("1.0.0.1", 443)`** and regular users to **`("1.1.1.1", 443)`**, all while maintaining and logging the counters. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Global Request Counter](#global-request-counter) + - [`MyProxy` Struct & `MyCtx` Context](#myproxy-struct--myctx-context) + - [Implementing `ProxyHttp` on `MyProxy`](#implementing-proxyhttp-on-myproxy) + 1. [Creating Context (`new_ctx`)](#1-creating-context-new_ctx) + 2. [Request Filter (`request_filter`)](#2-request-filter-request_filter) + 3. [Upstream Peer Selection (`upstream_peer`)](#3-upstream-peer-selection-upstream_peer) + - [Main Function & Server Setup](#main-function--server-setup) +3. [Testing the Proxy](#testing-the-proxy) +4. [Customization Ideas](#customization-ideas) + 1. [Enhanced Beta Logic](#1-enhanced-beta-logic) + 2. [Multiple Upstreams or Feature Flags](#2-multiple-upstreams-or-feature-flags) + 3. [Counter Storage Options](#3-counter-storage-options) +5. [Complete Example Code](#complete-example-code) +6. [Conclusion](#conclusion) + +--- + +## Introduction + +When proxying requests, you may want to **tag** certain users as “beta” (or “VIP,” or “internal”) based on some condition—here, a request header (`beta-flag`). This example: + +- Uses **thread-safe atomic counters** (from the standard library’s `std::sync::atomic`) to track: + - **Global** number of requests. + - **Beta** user requests. +- Prints log messages identifying how many total requests and how many beta requests have come in. + +--- + +## Code Overview + +### Global Request Counter + +```rust +static REQ_COUNTER: AtomicUsize = AtomicUsize::new(0); +``` + +- A **static** global variable to store the total request count. +- `AtomicUsize` allows us to increment it from multiple threads without additional locking. + +### `MyProxy` Struct & `MyCtx` Context + +```rust +pub struct MyProxy { + beta_counter: AtomicUsize, +} + +pub struct MyCtx { + beta_user: bool, +} +``` + +1. **`MyProxy`**: Holds a **beta_counter** that tracks requests specifically flagged as “beta.” +2. **`MyCtx`**: Per-request context storing whether the request is from a beta user (`beta_user: bool`). + +### Implementing `ProxyHttp` on `MyProxy` + +```rust +#[async_trait] +impl ProxyHttp for MyProxy { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { beta_user: false } + } + // ... +} +``` + +#### 1. Creating Context (`new_ctx`) + +```rust +fn new_ctx(&self) -> Self::CTX { + MyCtx { beta_user: false } +} +``` + +- Each request/connection gets a new `MyCtx`. +- Initially, we set `beta_user` to `false`; we’ll update it later. + +#### 2. Request Filter (`request_filter`) + +```rust +async fn request_filter(&self, session: &mut Session, ctx: &mut MyCtx) -> Result { + ctx.beta_user = check_beta_user(session.req_header()); + Ok(false) // Continue processing +} +``` + +- **`check_beta_user`** checks if the request header has a key `beta-flag`. +- We store the result in `ctx.beta_user`. +- Returning `Ok(false)` means “don’t short-circuit; keep going.” + +#### 3. Upstream Peer Selection (`upstream_peer`) + +```rust +async fn upstream_peer( + &self, + _session: &mut Session, + ctx: &mut MyCtx, +) -> Result> { + // Increment global request counter + let req_counter = REQ_COUNTER.fetch_add(1, Ordering::Relaxed) + 1; + + let addr = if ctx.beta_user { + // Increment beta counter + let beta_count = self.beta_counter.fetch_add(1, Ordering::Relaxed) + 1; + info!("I'm a beta user #{beta_count}"); + ("1.0.0.1", 443) + } else { + info!("I'm a user #{req_counter}"); + ("1.1.1.1", 443) + }; + + let peer = Box::new(HttpPeer::new( + addr, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) +} +``` + +- Atomically increments the **global** request counter (`REQ_COUNTER`). +- If `ctx.beta_user` is `true`, also increments the **beta** counter (`beta_counter`). +- Logs info either as “beta user” or “user” with the respective counter. +- Chooses the upstream address: + - Beta: **`1.0.0.1:443`** + - Regular: **`1.1.1.1:443`** + +--- + +## Main Function & Server Setup + +```rust +fn main() { + env_logger::init(); + + // Read command line arguments + let opt = Opt::parse_args(); + let mut my_server = Server::new(Some(opt)).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyProxy { + beta_counter: AtomicUsize::new(0), + }, + ); + my_proxy.add_tcp("0.0.0.0:6190"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +1. **`Opt::parse_args()`**: Reads any command line arguments (like `--help` or config file paths). +2. Creates a `Server` and calls `bootstrap()`. +3. **`MyProxy { beta_counter: AtomicUsize::new(0) }`** -> Our proxy instance. +4. We then add a TCP listener on **port 6190**. +5. Finally, **`run_forever()`** blocks the thread and runs the server until it’s shut down. + +--- + +## Testing the Proxy + +Compile and run: + +```bash +RUST_LOG=INFO cargo run +``` + +Then, issue some requests: + +1. **Regular user** (no `beta-flag`): + ```bash + curl -v http://127.0.0.1:6190/ -H "Host: one.one.one.one" + ``` + - Logs might show: **`I'm a user #1`**. + - Proxy routes to **`1.1.1.1:443`**. + +2. **Beta user** (with `beta-flag` header): + ```bash + curl -v http://127.0.0.1:6190/ -H "Host: one.one.one.one" -H "beta-flag: 1" + ``` + - Logs might show: **`I'm a beta user #1`**. + - Proxy routes to **`1.0.0.1:443`**. + +Check your console logs to confirm which path was chosen and watch the counters increment. + +--- + +## Customization Ideas + +### 1. Enhanced Beta Logic + +- Instead of checking for any `beta-flag`, parse an actual token or cookie. +- Possibly store user info in `ctx` (like user ID) if you need more advanced logic. + +### 2. Multiple Upstreams or Feature Flags + +- Add more upstream addresses if you want load balancing. +- Use additional headers or flags to pick a different cluster. +- e.g., a “gamma user” or “internal user” going to a new address. + +### 3. Counter Storage Options + +- Right now, we use **`AtomicUsize`**. If you want persistent storage, consider a database or an in-memory data store. +- For large-scale production, you might use metrics libraries like Prometheus to track these counters in a more robust way. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; +use log::info; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use pingora_core::server::configuration::Opt; +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::RequestHeader; +use pingora_proxy::{ProxyHttp, Session}; + +/// Global request counter using `AtomicUsize` for thread-safe atomic operations +static REQ_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub struct MyProxy { + /// Counter for beta users + beta_counter: AtomicUsize, +} + +pub struct MyCtx { + beta_user: bool, +} + +fn check_beta_user(req: &RequestHeader) -> bool { + // Simple logic to check if user is beta + req.headers.get("beta-flag").is_some() +} + +#[async_trait] +impl ProxyHttp for MyProxy { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { beta_user: false } + } + + async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result { + ctx.beta_user = check_beta_user(session.req_header()); + Ok(false) // Continue processing the request + } + + async fn upstream_peer( + &self, + _session: &mut Session, + ctx: &mut Self::CTX, + ) -> Result> { + // Increment the global request counter atomically + let req_counter = REQ_COUNTER.fetch_add(1, Ordering::Relaxed) + 1; + + let addr = if ctx.beta_user { + // Increment the beta user counter atomically + let beta_count = self.beta_counter.fetch_add(1, Ordering::Relaxed) + 1; + info!("I'm a beta user #{beta_count}"); + ("1.0.0.1", 443) + } else { + info!("I'm a user #{req_counter}"); + ("1.1.1.1", 443) + }; + + let peer = Box::new(HttpPeer::new( + addr, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +fn main() { + env_logger::init(); + + // Read command line arguments + let opt = Opt::parse_args(); + let mut my_server = Server::new(Some(opt)).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyProxy { + beta_counter: AtomicUsize::new(0), + }, + ); + my_proxy.add_tcp("0.0.0.0:6190"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +--- + +## Conclusion + +In this simple but powerful **Pingora** example, we differentiate between “beta” and “regular” users based on a header. We track each request via atomic counters, proving how easily **per-request context** and **global counters** can be integrated in a Rust proxy. Adapt this pattern to any scenario where you need to label users or distribute them across different upstreams. diff --git a/pingora_tutorial/gateway.md b/pingora_tutorial/gateway.md new file mode 100644 index 000000000..9a010ad47 --- /dev/null +++ b/pingora_tutorial/gateway.md @@ -0,0 +1,570 @@ + +# Building a Custom Reverse Proxy Gateway in Rust with Pingora + +In this tutorial, we’ll walk through a Rust-based gateway (reverse proxy) built on the [**Pingora**](https://docs.rs/pingora-core/latest/pingora_core/) ecosystem. By the end, you’ll understand how the code works, how it handles incoming requests, and how to customize it for your own needs—whether for logging, authentication, header manipulation, routing, or metrics collection. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Dependencies](#imports--dependencies) + - [`check_login` Function](#check_login-function) + - [`MyGateway` Struct](#mygateway-struct) + - [Implementing `ProxyHttp` Trait](#implementing-proxyhttp-trait) + 1. [Context](#1-context) + 2. [Request Filter](#2-request-filter) + 3. [Upstream Peer](#3-upstream-peer) + 4. [Response Filter](#4-response-filter) + 5. [Logging](#5-logging) +3. [Main Function & Server Setup](#main-function--server-setup) + - [Server Creation & Bootstrap](#server-creation--bootstrap) + - [Proxy & Service Setup](#proxy--service-setup) + - [Prometheus Metrics Service](#prometheus-metrics-service) + - [Running the Server Forever](#running-the-server-forever) +4. [How to Modify for Your Own Purposes](#how-to-modify-for-your-own-purposes) + 1. [Custom Authentication Logic](#1-custom-authentication-logic) + 2. [Different Upstream Servers](#2-different-upstream-servers) + 3. [Response Manipulation](#3-response-manipulation) + 4. [Additional Metrics & Logging](#4-additional-metrics--logging) + 5. [HTTPS vs. HTTP](#5-https-vs-http) +5. [Complete Example Code](#complete-example-code) +6. [Running and Testing](#running-and-testing) +7. [Conclusion](#conclusion) + +--- + +## Introduction + +A reverse proxy (or **gateway**) sits in front of your servers to control and process requests: + +- **Forward** client requests to the appropriate backend server (often called the “upstream”). +- Optionally **filter**, **modify**, or **inspect** requests and responses (e.g., authentication, header injection). +- **Measure metrics** (requests, latencies, errors, etc.) and **log** request details. + +Here, we use **Pingora**, a set of Rust crates that simplify building asynchronous proxy servers. You’ll see how we can wire up our own logic at various stages: before forwarding the request, after receiving the upstream response, and after sending the response back to the client. + +--- + +## Code Overview + +Below is an annotated version of our gateway code. + +### Imports & Dependencies + +```rust +use async_trait::async_trait; +use log::info; +use prometheus::{IntCounter, register_int_counter}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; +``` + +- **`async_trait`**: Makes it easier to define async methods in a trait implementation (like `ProxyHttp`). +- **`log::info`**: For logging. You can use `env_logger` or another logging solution for output. +- **`prometheus`**: For defining a Prometheus counter to track the number of requests. +- **`pingora_core`** and **`pingora_proxy`**: The core Pingora libraries that enable server creation, request filtering, upstream selection, and more. +- **`pingora_http`**: Provides types for HTTP request/response headers. + +### `check_login` Function + +```rust +fn check_login(req: &RequestHeader) -> bool { + // Implement your login check logic here + req.headers.get("Authorization") + .map(|v| v.as_bytes() == b"password") + .unwrap_or(false) +} +``` + +A simple function to check if the request has an `Authorization` header equal to `"password"`. +- For real production use, you would implement more robust logic (JWT validation, API keys, etc.). + +### `MyGateway` Struct + +```rust +pub struct MyGateway { + req_metric: IntCounter, +} +``` + +- We define a struct to hold any **gateway-wide** state or configuration. +- Here, it contains `req_metric`, a Prometheus counter that we’ll increment on each request. + +### Implementing `ProxyHttp` Trait + +This trait tells Pingora **how** to process requests at each stage of the proxy cycle. + +#### 1. Context + +```rust +type CTX = (); + +fn new_ctx(&self) -> Self::CTX { + () +} +``` + +- **`type CTX`**: Defines the session context type. Here, we use the unit type `()`, meaning our per-session context is empty. +- **`new_ctx`**: Each incoming connection creates a new context. You can store per-request data here (e.g., authentication info, counters, etc.). + +#### 2. Request Filter + +```rust +async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { + if session.req_header().uri.path().starts_with("/login") + && !check_login(session.req_header()) + { + let _ = session.respond_error(403).await; + // Return true to indicate early response + return Ok(true); + } + Ok(false) +} +``` + +Called **before** we select an upstream. We can choose to **short-circuit** (e.g., respond with an error) and not forward the request at all. + +- If the URI path starts with `/login` and the user fails `check_login`, respond with `403 Forbidden` and return `Ok(true)`. + - Returning `true` means “**I already sent a response, stop processing**.” + +#### 3. Upstream Peer + +```rust +async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result> { + let addr = if session.req_header().uri.path().starts_with("/family") { + ("1.0.0.1", 443) + } else { + ("1.1.1.1", 443) + }; + + info!("Connecting to {:?}", addr); + + let peer = Box::new(HttpPeer::new( + addr, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) +} +``` + +Tells Pingora **where** to forward requests. You return a [`HttpPeer`](https://docs.rs/pingora-core/latest/pingora_core/upstreams/peer/struct.HttpPeer.html) that indicates: + +- The IP/port to connect to (`addr`). +- Whether it’s **HTTPS** (`true`) or **HTTP** (`false`). +- The hostname used for SNI (`"one.one.one.one".to_string()`). + +In this sample, `/family` is routed to `1.0.0.1:443` and everything else goes to `1.1.1.1:443` (Cloudflare addresses). You could, for example, change this to `("127.0.0.1", 3000)` if you have a local server on port 3000. + +#### 4. Response Filter + +```rust +async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, +) -> Result<()> { + // Replace existing header if any + upstream_response + .insert_header("Server", "MyGateway") + .unwrap(); + // Remove unsupported header + upstream_response.remove_header("alt-svc"); + + Ok(()) +} +``` + +After receiving the **upstream** response but before sending it back to the client, we can modify it: + +- Insert or replace the `Server` header with `"MyGateway"`. +- Remove the `alt-svc` header. + +#### 5. Logging + +```rust +async fn logging( + &self, + session: &mut Session, + _e: Option<&pingora_core::Error>, + _ctx: &mut Self::CTX, +) { + let response_code = session + .response_written() + .map_or(0, |resp| resp.status.as_u16()); + info!( + "Request to {} responded with status code {}", + session.req_header().uri.path(), + response_code + ); + + self.req_metric.inc(); +} +``` + +This is called **after** the response is written to the client, whether successful or not. Common uses: + +- **Log** the request/response details. +- **Record metrics** (in this case, increment a Prometheus counter). + +--- + +## Main Function & Server Setup + +Here’s where we initialize and run the Pingora server: + +```rust +fn main() { + env_logger::init(); + + // Create the server without options + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let req_metric = register_int_counter!("req_counter", "Number of requests").unwrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyGateway { + req_metric, + }, + ); + my_proxy.add_tcp("0.0.0.0:6191"); + my_server.add_service(my_proxy); + + let mut prometheus_service_http = + pingora_core::services::listening::Service::prometheus_http_service(); + prometheus_service_http.add_tcp("127.0.0.1:6192"); + my_server.add_service(prometheus_service_http); + + my_server.run_forever(); +} +``` + +### Server Creation & Bootstrap + +```rust +let mut my_server = Server::new(None).unwrap(); +my_server.bootstrap(); +``` + +- Creates a `Server` with an optional configuration (here, `None`). +- `bootstrap()` typically sets up internal tasks or resources. + +### Proxy & Service Setup + +```rust +let req_metric = register_int_counter!("req_counter", "Number of requests").unwrap(); + +let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyGateway { + req_metric, + }, +); +my_proxy.add_tcp("0.0.0.0:6191"); +my_server.add_service(my_proxy); +``` + +- We create a Prometheus counter called `"req_counter"`. +- Create an **HTTP proxy service** using our `MyGateway` struct, passing in the Pingora configuration and the newly created counter. +- **`add_tcp("0.0.0.0:6191")`** means this proxy listens on port `6191` for incoming traffic. +- Finally, add this proxy to the server as a **service**. + +### Prometheus Metrics Service + +```rust +let mut prometheus_service_http = + pingora_core::services::listening::Service::prometheus_http_service(); +prometheus_service_http.add_tcp("127.0.0.1:6192"); +my_server.add_service(prometheus_service_http); +``` + +- Pingora has a built-in Prometheus service that automatically collects metrics from the `prometheus` crate. +- Listening on `127.0.0.1:6192` means you can access metrics at `http://127.0.0.1:6192/metrics`. + +### Running the Server Forever + +```rust +my_server.run_forever(); +``` + +- This blocks the current thread and starts the event loop. +- The server will run until the process is killed or otherwise stopped. + +--- + +## How to Modify for Your Own Purposes + +### 1. Custom Authentication Logic + +The current example checks if `Authorization` equals `"password"`. For production, you might: + +- Check for a **JWT** token. +- Verify a **Basic Auth** credential. +- Query a database or third-party service to validate a user. + +Just update the `check_login` function and the `request_filter` logic accordingly: + +```rust +fn check_login(req: &RequestHeader) -> bool { + // Example of checking a custom Bearer token + if let Some(auth_header) = req.headers.get("Authorization") { + // parse "Bearer " + // validate somehow + } + false +} +``` + +### 2. Different Upstream Servers + +Instead of `1.1.1.1:443`, you might have multiple backend services or a local server. Adjust `upstream_peer`: + +```rust +async fn upstream_peer(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result> { + // Example: forward everything to local server at 127.0.0.1:3000, plain HTTP: + let addr = ("127.0.0.1", 3000); + let peer = Box::new(HttpPeer::new(addr, false, "127.0.0.1".to_string())); + Ok(peer) +} +``` + +You can also do path-based routing or choose the upstream dynamically based on headers, cookies, load balancing, etc. + +### 3. Response Manipulation + +Currently, we set `Server: MyGateway` and remove `alt-svc`. You could: + +- Add or remove **other** headers like `X-Frame-Options`, `Content-Security-Policy`, or `Location`. +- Modify **status codes** or handle edge cases (e.g. certain 5xx responses). +- Cache responses or rewrite body content (though you’d need additional logic to manage the body stream). + +### 4. Additional Metrics & Logging + +Inside the `logging` method (or throughout other methods), you can: + +- **Log** request paths, times, user agents, etc. +- Create **Prometheus histograms** to measure response times. For example: + + ```rust + let histogram = register_histogram!("request_duration_seconds", "Request duration").unwrap(); + // Then measure how long each request takes + ``` + +### 5. HTTPS vs. HTTP + +In `HttpPeer::new(addr, use_tls, sni_hostname)`: + +- If you connect to an **HTTPS** server, set `use_tls` to `true`, and provide a real hostname for TLS SNI. +- If **HTTP**, set `use_tls` to `false`. + +For local servers, you typically do `false`, e.g.: + +```rust +HttpPeer::new(("127.0.0.1", 3000), false, "127.0.0.1".to_string()) +``` + +--- + +## Complete Example Code + +Below is the full code for the gateway: + +```rust +use async_trait::async_trait; +use log::info; +use prometheus::{IntCounter, register_int_counter}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; + +fn check_login(req: &RequestHeader) -> bool { + // Implement your login check logic here + req.headers + .get("Authorization") + .map(|v| v.as_bytes() == b"password") + .unwrap_or(false) +} + +pub struct MyGateway { + req_metric: IntCounter, +} + +#[async_trait] +impl ProxyHttp for MyGateway { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { + if session.req_header().uri.path().starts_with("/login") + && !check_login(session.req_header()) + { + let _ = session.respond_error(403).await; + // Return true to indicate early response + return Ok(true); + } + Ok(false) + } + + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let addr = if session.req_header().uri.path().starts_with("/family") { + ("1.0.0.1", 443) + } else { + ("1.1.1.1", 443) + }; + + info!("Connecting to {:?}", addr); + + let peer = Box::new(HttpPeer::new( + addr, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } + + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_response + .insert_header("Server", "MyGateway") + .unwrap(); + upstream_response.remove_header("alt-svc"); + Ok(()) + } + + async fn logging( + &self, + session: &mut Session, + _e: Option<&pingora_core::Error>, + _ctx: &mut Self::CTX, + ) { + let response_code = session + .response_written() + .map_or(0, |resp| resp.status.as_u16()); + info!( + "Request to {} responded with status code {}", + session.req_header().uri.path(), + response_code + ); + + self.req_metric.inc(); + } +} + +fn main() { + env_logger::init(); + + // Create the server without options + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let req_metric = register_int_counter!("req_counter", "Number of requests").unwrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyGateway { + req_metric, + }, + ); + my_proxy.add_tcp("0.0.0.0:6191"); + my_server.add_service(my_proxy); + + let mut prometheus_service_http = + pingora_core::services::listening::Service::prometheus_http_service(); + prometheus_service_http.add_tcp("127.0.0.1:6192"); + my_server.add_service(prometheus_service_http); + + my_server.run_forever(); +} +``` + +--- + +## Running and Testing + +1. **Add Dependencies**: In your `Cargo.toml`, ensure you have the relevant crates: + + ```toml + [dependencies] + async-trait = "0.1" + env_logger = "0.9" + log = "0.4" + prometheus = "0.14" + pingora-core = "0.1" + pingora-proxy = "0.1" + pingora-http = "0.1" + # (Check crates.io or docs.rs for exact versions) + ``` + +2. **Build and Run**: + + ```bash + cargo run + ``` + + You’ll see log output, and the gateway will listen on `0.0.0.0:6191`. + +3. **Test**: + + - Open another terminal and run: + ```bash + curl -v http://localhost:6191/login + ``` + You should get a `403 Forbidden` if you don’t send the `Authorization: password` header. + + ```bash + curl -v -H "Authorization: password" http://localhost:6191/login + ``` + Should forward the request upstream (in this example, to `1.1.1.1` by default). + + - **Prometheus Metrics**: + ```bash + curl -v http://localhost:6192/metrics + ``` + You should see various metrics, including `req_counter`. + +--- + +## Conclusion + +With **Pingora**, you can create a highly customizable reverse proxy in Rust, handling authentication, routing, logging, and more. By modifying the methods in the `ProxyHttp` trait, you can: + +- Decide whether to drop or alter requests (`request_filter`). +- Dynamically choose an upstream target (`upstream_peer`). +- Inject or remove headers in the response (`response_filter`). +- Log request details and record metrics (`logging`). + +For real-world scenarios, you might add: + +- Complex **load balancing** across multiple backends. +- **Caching** of responses. +- **Rate limiting** or **access control**. +- **TLS termination** if you need to handle HTTPS traffic directly. + +Feel free to build on this foundation, add your own logic, and deploy a production-ready Rust gateway tailored to your needs. diff --git a/pingora_tutorial/grpc_web_module.md b/pingora_tutorial/grpc_web_module.md new file mode 100644 index 000000000..a95eaf066 --- /dev/null +++ b/pingora_tutorial/grpc_web_module.md @@ -0,0 +1,309 @@ +# Tutorial: Bridging gRPC-Web Traffic with Pingora’s `GrpcWebBridge` Module + +This example demonstrates how to **convert gRPC-web requests** into standard gRPC calls using the [Pingora](https://docs.rs/pingora-core) library’s **`GrpcWebBridge`** module. This is especially useful when you have front-end code (such as a browser client) that communicates via **gRPC-web**, but your backend server expects normal gRPC over HTTP/2. + +Below, we’ll walk through: + +1. **How the code works**—hooking into Pingora’s module system to handle gRPC-web. +2. **Where to point the upstream**—i.e., the actual gRPC server address. +3. **Modifying the code** to link it with another test server (like one you’ve built in an earlier example). + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Setup](#imports--setup) + - [`GrpcWebBridgeProxy` Struct](#grpcwebbridgeproxy-struct) + 1. [Module Initialization](#1-module-initialization) + 2. [Early Request Filter](#2-early-request-filter) + 3. [Upstream Peer](#3-upstream-peer) + - [Main Function & Server Setup](#main-function--server-setup) +3. [Connecting to a Test gRPC Server](#connecting-to-a-test-grpc-server) +4. [Modifying for Other Upstreams or Local Test Servers](#modifying-for-other-upstreams-or-local-test-servers) +5. [Complete Example Code](#complete-example-code) +6. [Running & Testing](#running--testing) + +--- + +## Introduction + +**gRPC-web** is a variant of gRPC protocol that works in modern browsers. However, your backend might only speak **standard gRPC**. This code uses Pingora to: + +- Accept **gRPC-web** requests from the client. +- Use the **`GrpcWebBridge`** module to unwrap the gRPC-web request format into a standard gRPC request. +- Forward it to the real gRPC server (e.g., `1.1.1.1:443`). +- Convert the gRPC response back into **gRPC-web** format before returning to the browser. + +--- + +## Code Overview + +Below is a step-by-step guide to the relevant parts of the code. + +### Imports & Setup + +```rust +use async_trait::async_trait; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + modules::http::{ + grpc_web::{GrpcWeb, GrpcWebBridge}, + HttpModules, + }, +}; +use pingora_proxy::{ProxyHttp, Session}; +``` + +Key points: + +- **`grpc_web::{GrpcWeb, GrpcWebBridge}`**: The Pingora module for bridging gRPC-web to standard gRPC. +- **`ProxyHttp`**: The main trait we implement to define our custom proxy logic. +- **`HttpModules`**: A registry where we attach our modules (like `GrpcWeb`). + +### `GrpcWebBridgeProxy` Struct + +```rust +pub struct GrpcWebBridgeProxy; +``` + +A simple unit struct that implements `ProxyHttp`. + +#### 1. Module Initialization + +```rust +fn init_downstream_modules(&self, modules: &mut HttpModules) { + // Add the gRPC web module + modules.add_module(Box::new(GrpcWeb)) +} +``` + +- **`init_downstream_modules`** is called **once** when the server starts. +- We add the **`GrpcWeb`** module, which allows the proxy to parse gRPC-web requests and wrap them in a **`GrpcWebBridge`** context. + +#### 2. Early Request Filter + +```rust +async fn early_request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result<()> { + let grpc = session + .downstream_modules_ctx + .get_mut::() + .expect("GrpcWebBridge module added"); + + // Initialize gRPC module for this request + grpc.init(); + Ok(()) +} +``` + +- **`early_request_filter`** is called before other filters. +- We grab the **`GrpcWebBridge`** context (inserted by the `GrpcWeb` module) and call `init()` on it. This sets up the bridging logic for this individual request. + +#### 3. Upstream Peer + +```rust +async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result> { + let grpc_peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(grpc_peer) +} +``` + +- Specifies **where** to forward the request—here, `"1.1.1.1:443"`. +- The second parameter (`true`) indicates **TLS**, and `"one.one.one.one"` is the **SNI** hostname. +- You would replace this with the actual address of your **gRPC server**. + +--- + +## Main Function & Server Setup + +```rust +fn main() { + env_logger::init(); + + // Create server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = + pingora_proxy::http_proxy_service(&my_server.configuration, GrpcWebBridgeProxy); + my_proxy.add_tcp("0.0.0.0:6194"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +1. **Create the Pingora `Server`** and call `bootstrap()`. +2. **Create** an HTTP proxy service with `GrpcWebBridgeProxy`. +3. **Listen** on port `6194`. +4. **Add** the service to the server and call `run_forever()` to block. + +At this point, the proxy is listening on `0.0.0.0:6194`, expecting gRPC-web requests. + +--- + +## Connecting to a Test gRPC Server + +If you have a local gRPC server running (for example, **`test_server`** from a previous example or a separate codebase), you can **change** the `upstream_peer` method to point to it. For instance: + +```rust +async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result> { + // If your test gRPC server is on localhost:50051 + let grpc_peer = Box::new(HttpPeer::new( + ("127.0.0.1", 50051), + false, // false if your server doesn't use TLS + "localhost".to_string(), + )); + Ok(grpc_peer) +} +``` + +- **`("127.0.0.1", 50051)`**: Adjust host/port to match your local server. +- Set the second argument to `false` if your server is plain HTTP/2 (non-TLS). +- Provide an SNI hostname if TLS is `true`; otherwise, it can be a placeholder. + +When the code references **`test_server`**, make sure that **test_server** is indeed **a gRPC server** or gRPC-enabled. If it’s a plain HTTP server, bridging gRPC-web traffic to it won’t work as intended, because the bridging module expects standard gRPC frames. + +--- + +## Modifying for Other Upstreams or Local Test Servers + +1. **Different Remote gRPC**: If you have a remote gRPC server at `grpc.example.com:443`, swap in those details: + ```rust + let grpc_peer = Box::new(HttpPeer::new( + ("grpc.example.com", 443), + true, + "grpc.example.com".to_string(), + )); + ``` +2. **Local Environments**: If your server doesn’t use TLS, set the second parameter to `false`: + ```rust + let grpc_peer = Box::new(HttpPeer::new( + ("127.0.0.1", 50051), + false, + "127.0.0.1".to_string(), + )); + ``` +3. **Custom bridging**: If you want to do something else inside `early_request_filter`, like logging or extra header checks, you can do so before calling `grpc.init()`. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + modules::http::{ + grpc_web::{GrpcWeb, GrpcWebBridge}, + HttpModules, + }, +}; +use pingora_proxy::{ProxyHttp, Session}; + +/// This example shows how to use the gRPC-web bridge module + +pub struct GrpcWebBridgeProxy; + +#[async_trait] +impl ProxyHttp for GrpcWebBridgeProxy { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + fn init_downstream_modules(&self, modules: &mut HttpModules) { + // Add the gRPC web module + modules.add_module(Box::new(GrpcWeb)) + } + + async fn early_request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result<()> { + let grpc = session + .downstream_modules_ctx + .get_mut::() + .expect("GrpcWebBridge module added"); + + // Initialize gRPC module for this request + grpc.init(); + Ok(()) + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // Replace this address with your gRPC server + let grpc_peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(grpc_peer) + } +} + +fn main() { + env_logger::init(); + + // Create server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = + pingora_proxy::http_proxy_service(&my_server.configuration, GrpcWebBridgeProxy); + my_proxy.add_tcp("0.0.0.0:6194"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +--- + +## Running & Testing + +1. **Start the Proxy**: + ```bash + cargo run + ``` + The proxy will listen on `0.0.0.0:6194`. + +2. **Configure Your Front-End**: + - Set your browser client or gRPC-web client to send requests to `http://:6194`. + - The `GrpcWeb` module handles the translation from gRPC-web to standard gRPC. + +3. **Check Upstream**: + - If your local server runs at `127.0.0.1:50051`, switch the code in `upstream_peer` accordingly. + - Ensure your real gRPC server is running and accepts requests. + +4. **Integration with a Test Server**: + - If you previously wrote a **gRPC test server** in Rust or another language, just ensure that it’s listening on a known host/port and that you adjust the `HttpPeer::new(...)` arguments here. That will let the bridging proxy connect to the test server seamlessly. + +You can now send gRPC-web requests to port **6194**, and they will be bridged to your gRPC server. If everything is set up correctly, you’ll see successful traffic going through. + +**That’s it!** You have a working gRPC-web bridge in Rust using Pingora, ready to adapt to your real-world gRPC environment. diff --git a/pingora_tutorial/load_loadbalancer.md b/pingora_tutorial/load_loadbalancer.md new file mode 100644 index 000000000..ea5b50782 --- /dev/null +++ b/pingora_tutorial/load_loadbalancer.md @@ -0,0 +1,366 @@ +# Tutorial: Using a Load Balancer with Health Checks and TLS in Pingora + +This example demonstrates how to set up a **load-balanced** HTTP proxy with: +- **Multiple upstream servers**, including a “bad” server that will fail health checks. +- **TCP health checks** that remove unhealthy servers from rotation automatically. +- **TLS listeners** configured for HTTP/2 (and HTTP/1.1 over TLS). + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Setup](#imports--setup) + - [Defining the `LB` Struct](#defining-the-lb-struct) + - [Implementing the `ProxyHttp` Trait on `LB`]](#implementing-the-proxyhttp-trait-on-lb) + 1. [`upstream_peer` Method](#1-upstream_peer-method) + 2. [`upstream_request_filter` Method](#2-upstream_request_filter-method) + - [Main Function & Server Setup](#main-function--server-setup) + 1. [Creating the Server & Bootstrapping](#1-creating-the-server--bootstrapping) + 2. [Configuring the Load Balancer & Health Checks](#2-configuring-the-load-balancer--health-checks) + 3. [Background Service for Health Checks](#3-background-service-for-health-checks) + 4. [Proxy Service & TLS Configuration](#4-proxy-service--tls-configuration) +3. [Modifying the Code](#modifying-the-code) + 1. [Adding More Upstreams or Changing Their Addresses](#1-adding-more-upstreams-or-changing-their-addresses) + 2. [Adjusting the Health Check Logic](#2-adjusting-the-health-check-logic) + 3. [Changing TLS Settings and Certificates](#3-changing-tls-settings-and-certificates) + 4. [Connecting to a Test Server Locally](#4-connecting-to-a-test-server-locally) +4. [Complete Example Code](#complete-example-code) +5. [Running & Testing](#running--testing) +6. [Conclusion](#conclusion) + +--- + +## Introduction + +**Load balancing** in Pingora allows you to distribute incoming requests across multiple upstream servers. By combining **health checks**, you can automatically avoid sending traffic to servers that are down or misbehaving. Additionally, Pingora makes it simple to expose both **plaintext** and **TLS** endpoints, supporting HTTP/1.1 and HTTP/2. + +--- + +## Code Overview + +Below, we’ll highlight each section of the code that creates a load balancer with three upstreams, sets up TCP health checks, and configures two listeners: +- **Port 6188** for plaintext traffic. +- **Port 6189** for TLS-encrypted traffic. + +### Imports & Setup + +```rust +use async_trait::async_trait; +use log::info; +use pingora_core::services::background::background_service; +use std::{sync::Arc, time::Duration}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + listeners::tls::TlsSettings, +}; +use pingora_load_balancing::{health_check, selection::RoundRobin, LoadBalancer}; +use pingora_proxy::{ProxyHttp, Session}; +use pingora_http::RequestHeader; +``` + +- **`pingora_core::services::background::background_service`**: Allows us to run health checks in the background. +- **`pingora_load_balancing::{health_check, selection::RoundRobin, LoadBalancer}`**: Key load balancing constructs, including the `TcpHealthCheck` type and a `RoundRobin` strategy. +- **`pingora_core::listeners::tls::TlsSettings`**: Lets us configure TLS for a listening port. + +### Defining the `LB` Struct + +```rust +pub struct LB(Arc>); +``` + +- Holds an **Arc** reference to the `LoadBalancer`. +- We’ll implement `ProxyHttp` on this struct to define how traffic is routed. + +### Implementing the `ProxyHttp` Trait on `LB` + +#### 1. `upstream_peer` Method + +```rust +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result> { + let upstream = self + .0 + .select(b"", 256) // hash doesn't matter here + .unwrap(); + + info!("upstream peer is: {:?}", upstream); + + let peer = Box::new(HttpPeer::new(upstream, true, "one.one.one.one".to_string())); + Ok(peer) + } + + // ... +} +``` + +- **`select(b"", 256)`** picks an upstream from the load balancer using **RoundRobin** strategy. +- The second parameter `256` is the maximum tries if the first one is unhealthy. +- We create a `HttpPeer` with TLS (`true`) and an SNI hostname `"one.one.one.one"`. + - Replace `("one.one.one.one")` with the real SNI host if needed, or set TLS to `false` for HTTP. + +#### 2. `upstream_request_filter` Method + +```rust +async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, +) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) +} +``` + +- Before sending the request upstream, we set `Host` to `"one.one.one.one"`. +- In a production scenario, change this to your real upstream host. + +--- + +## Main Function & Server Setup + +```rust +fn main() { + env_logger::init(); + + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + // ... +} +``` + +### 1. Creating the Server & Bootstrapping + +```rust +let mut my_server = Server::new(None).unwrap(); +my_server.bootstrap(); +``` + +- **`Server::new(None)`** starts a Pingora server with no custom config. +- **`bootstrap()`** prepares background tasks or other internal logic. + +### 2. Configuring the Load Balancer & Health Checks + +```rust +let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443", "127.0.0.1:343"]).unwrap(); +``` + +- We pass a slice of upstream addresses. Note that `"127.0.0.1:343"` is deliberately a “bad” server. +- `try_from_iter` creates a `LoadBalancer` by default. + +```rust +let hc = health_check::TcpHealthCheck::new(); +upstreams.set_health_check(hc); +upstreams.health_check_frequency = Some(Duration::from_secs(1)); +``` + +- **TCP** health check tries to connect to each server. If it fails, that server is marked unhealthy. +- **`health_check_frequency`** determines how often the checks run (every 1 second here). + +### 3. Background Service for Health Checks + +```rust +let background = background_service("health check", upstreams); +let upstreams = background.task(); +``` + +- **`background_service`** spawns a task to handle these checks in the background. +- We then retrieve an `Arc>` via `background.task()` to share with our proxy. + +### 4. Proxy Service & TLS Configuration + +```rust +let mut lb = pingora_proxy::http_proxy_service(&my_server.configuration, LB(upstreams)); +lb.add_tcp("0.0.0.0:6188"); +``` + +- We create an **HTTP proxy** and bind it to `0.0.0.0:6188` for plaintext connections. + +```rust +let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); +let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); + +let mut tls_settings = TlsSettings::intermediate(&cert_path, &key_path).unwrap(); +tls_settings.enable_h2(); +lb.add_tls_with_settings("0.0.0.0:6189", None, tls_settings); +``` + +- We load a self-signed certificate and key (in the `tests/keys` folder). +- **`TlsSettings::intermediate`** applies a default TLS security policy. +- **`enable_h2()`** allows HTTP/2 connections. +- We bind TLS on **port 6189**. + +Finally, we register both the **proxy service** and the **background** health check service with `my_server`: + +```rust +my_server.add_service(lb); +my_server.add_service(background); +my_server.run_forever(); +``` + +--- + +## Modifying the Code + +### 1. Adding More Upstreams or Changing Their Addresses + +- Swap out `["1.1.1.1:443", "1.0.0.1:443"]` for your own servers. +- You can include additional addresses like `["service1:443", "service2:443"]`. + +### 2. Adjusting the Health Check Logic + +- If you want an **HTTP** health check (sending a GET to `/health` endpoint), you’d need to implement a different `TcpHealthCheck` or use an **HTTP** approach. +- Increase or decrease `health_check_frequency` to reduce overhead or improve detection times. + +### 3. Changing TLS Settings and Certificates + +- To support a real certificate, put the correct paths to your `.crt` and `.key` files. +- If you want a stricter or custom policy (e.g., requiring client certs), explore Pingora’s `TlsSettings` methods. + +### 4. Connecting to a Test Server Locally + +- If you have a local server on `127.0.0.1:3000`, switch the upstream list. +- Update **`upstream_request_filter`** if you need a different `Host` header. +- In a testing scenario, you can comment out the TLS code if you just want to confirm load balancing without SSL overhead. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; +use log::info; +use pingora_core::services::background::background_service; +use std::{sync::Arc, time::Duration}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + listeners::tls::TlsSettings, +}; +use pingora_load_balancing::{health_check, selection::RoundRobin, LoadBalancer}; +use pingora_proxy::{ProxyHttp, Session}; +use pingora_http::RequestHeader; + +pub struct LB(Arc>); + +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result> { + let upstream = self + .0 + .select(b"", 256) + .unwrap(); + + info!("upstream peer is: {:?}", upstream); + + // Indicate TLS = true, with SNI "one.one.one.one" + let peer = Box::new(HttpPeer::new(upstream, true, "one.one.one.one".to_string())); + Ok(peer) + } + + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) + } +} + +fn main() { + env_logger::init(); + + // Create the server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + // "127.0.0.1:343" is intentionally a bad server + let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443", "127.0.0.1:343"]).unwrap(); + + // We add health checks to automatically exclude unhealthy servers + let hc = health_check::TcpHealthCheck::new(); + upstreams.set_health_check(hc); + upstreams.health_check_frequency = Some(Duration::from_secs(1)); + + // Start the health checks in the background + let background = background_service("health check", upstreams); + let upstreams = background.task(); + + // Create a proxy service using LB + let mut lb = pingora_proxy::http_proxy_service(&my_server.configuration, LB(upstreams)); + // Plaintext listener on port 6188 + lb.add_tcp("0.0.0.0:6188"); + + // Setup TLS on port 6189 + let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); + let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); + let mut tls_settings = TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + tls_settings.enable_h2(); + lb.add_tls_with_settings("0.0.0.0:6189", None, tls_settings); + + // Add the proxy service and background service to the server + my_server.add_service(lb); + my_server.add_service(background); + my_server.run_forever(); +} +``` + +--- + +## Running & Testing + +1. **Build and Run**: + ```bash + cargo run + ``` + Make sure `server.crt` and `key.pem` exist at the specified paths (`tests/keys`), or adjust the paths. + +2. **Plaintext Test**: + - Send requests to `http://127.0.0.1:6188/`. + - The proxy will round-robin between `1.1.1.1:443` and `1.0.0.1:443`, skipping `127.0.0.1:343` after health checks fail. + +3. **TLS Test**: + - Use `curl --insecure https://127.0.0.1:6189/` if using self-signed certs. + - This tests the TLS listener; traffic is still proxied upstream with a TLS handshake to `1.1.1.1`. + +4. **Observe Logs**: + - You should see logs indicating which upstream is selected. + - After a second, the bad server (`127.0.0.1:343`) should be marked unhealthy. + +--- + +## Conclusion + +With this Pingora-based setup, you can: + +- **Automatically** exclude unhealthy servers from your load-balancing pool. +- Expose both **plaintext** (HTTP) and **TLS** (HTTPS) endpoints, letting clients connect over their preferred protocols. +- Customize health checks, load balancing strategies, and TLS settings to match your production environment. + +Feel free to **modify** the code to add more advanced routing, more robust health checks (HTTP-based), or different TLS/cert configurations. Good luck building your next load-balanced Rust proxy with Pingora! diff --git a/pingora_tutorial/modify_response.md b/pingora_tutorial/modify_response.md new file mode 100644 index 000000000..51009e6c1 --- /dev/null +++ b/pingora_tutorial/modify_response.md @@ -0,0 +1,383 @@ +# Tutorial: Transforming JSON Responses to YAML via Pingora Proxy + +This code demonstrates a **Pingora**-based Rust proxy that **fetches JSON** from an upstream server, **transforms** it into **YAML**, and returns the YAML body to the client. Along the way, you’ll see how to: + +1. Implement the `ProxyHttp` trait to define custom request/response handling. +2. **Rewrite** the response body by buffering data, deserializing JSON, and then serializing it to YAML. +3. Adjust headers to ensure the client accepts the modified body correctly (e.g., removing `Content-Length` and setting `Transfer-Encoding: chunked`). + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Dependencies](#imports--dependencies) + - [Structs: `Json2Yaml` & `MyCtx`](#structs-json2yaml--myctx) + - [Implementing `ProxyHttp` for `Json2Yaml`](#implementing-proxyhttp-for-json2yaml) + 1. [Context & Upstream Selection](#1-context--upstream-selection) + 2. [Upstream Request Filter](#2-upstream-request-filter) + 3. [Response Filter (Headers)](#3-response-filter-headers) + 4. [Response Body Filter (Transform JSON to YAML)](#4-response-body-filter-transform-json-to-yaml) + - [`main` Function & Running the Server](#main-function--running-the-server) +3. [Testing the Proxy](#testing-the-proxy) +4. [Modifying the Code](#modifying-the-code) + - [Different Upstream Host](#different-upstream-host) + - [Other Transformations (XML, CSV, etc.)](#other-transformations-xml-csv-etc) + - [Body Streaming vs. Buffering](#body-streaming-vs-buffering) +5. [Complete Example Code](#complete-example-code) +6. [Conclusion](#conclusion) + +--- + +## Introduction + +**Pingora** is a Rust-based framework for building custom proxies. In this snippet, we use Pingora to: + +- **Connect** to an HTTP endpoint (in this case, `ip.jsontest.com` which returns a JSON containing your IP). +- **Receive** the JSON response. +- **Parse** it into a Rust struct (`Resp`). +- **Serialize** it as **YAML**. +- **Send** the transformed YAML back to the client. + +--- + +## Code Overview + +### Imports & Dependencies + +```rust +use async_trait::async_trait; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; +``` + +Key libraries: + +- **`serde`, `serde_json`, `serde_yaml`** (via `serde` and `serde_yaml` crates) for serialization and deserialization. +- **`bytes::Bytes`** to handle chunked data in memory. +- **`Pingora`** components for proxy logic, servers, and HTTP peers. + +### Structs: `Json2Yaml` & `MyCtx` + +```rust +pub struct Json2Yaml { + addr: std::net::SocketAddr, +} + +pub struct MyCtx { + buffer: Vec, +} +``` + +1. **`Json2Yaml`**: Holds a `std::net::SocketAddr` for the upstream server. We’ll implement `ProxyHttp` on this struct. +2. **`MyCtx`**: A per-connection (or per-request) context storing a buffer `Vec` where we accumulate the response body before converting to YAML. + +### Implementing `ProxyHttp` for `Json2Yaml` + +```rust +#[async_trait] +impl ProxyHttp for Json2Yaml { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { buffer: vec![] } + } + ... +} +``` + +#### 1. Context & Upstream Selection + +```rust +async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result> { + let peer = Box::new(HttpPeer::new(self.addr, false, HOST.to_owned())); + Ok(peer) +} +``` + +- **`upstream_peer`**: Chooses the upstream server. + - `false` => Not using TLS (it’s plain HTTP). + - `HOST` is `"ip.jsontest.com"`, and `addr` is `142.251.2.121:80` in the example. +- You can change these to point to a different server. + +#### 2. Upstream Request Filter + +```rust +async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, +) -> Result<()> { + upstream_request.insert_header("Host", HOST).unwrap(); + Ok(()) +} +``` + +- Before sending the request upstream, we set the `Host` header to match `ip.jsontest.com`. +- This is required if the remote server relies on the Host header to dispatch requests. + +#### 3. Response Filter (Headers) + +```rust +async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, +) -> Result<()> { + // Remove Content-Length because the size of the new body is unknown + upstream_response.remove_header("Content-Length"); + upstream_response.insert_header("Transfer-Encoding", "chunked").unwrap(); + Ok(()) +} +``` + +- We remove the `Content-Length` because we’re changing the body size. +- Then, we set **`Transfer-Encoding: chunked`** so the client can read data in streaming chunks rather than expecting a fixed length. + +#### 4. Response Body Filter (Transform JSON to YAML) + +```rust +fn response_body_filter( + &self, + _session: &mut Session, + body: &mut Option, + end_of_stream: bool, + ctx: &mut Self::CTX, +) -> Result> { + // Accumulate data in the context buffer + if let Some(b) = body.take() { + ctx.buffer.extend_from_slice(&b); + } + + if end_of_stream { + // The upstream finished sending data + let json_body: Resp = serde_json::from_slice(&ctx.buffer).unwrap(); + let yaml_body = serde_yaml::to_string(&json_body).unwrap(); + + // Replace the body with the new YAML data + *body = Some(Bytes::copy_from_slice(yaml_body.as_bytes())); + } + + Ok(None) +} +``` + +- **Buffering**: We store the body chunks in `ctx.buffer` until the entire response is received (`end_of_stream == true`). +- **Transform**: Once we have the full JSON body, we deserialize into `Resp` and then serialize to YAML. +- Finally, we replace the body with the new YAML-encoded data. + +--- + +## `main` Function & Running the Server + +```rust +fn main() { + env_logger::init(); + + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + Json2Yaml { + addr: "142.251.2.121:80".parse().unwrap(), + }, + ); + my_proxy.add_tcp("127.0.0.1:6191"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +1. **`Server::new(None)`** creates a Pingora server. +2. **`bootstrap()`** readies internal tasks. +3. **`Json2Yaml`** instance: Hard-coded to `142.251.2.121:80`, which is one of Google IPs for `ip.jsontest.com`. +4. **`my_proxy.add_tcp("127.0.0.1:6191")`** => The proxy listens on port **6191** for incoming connections. +5. **`my_server.run_forever()`** => Start the event loop and serve until interrupted. + +--- + +## Testing the Proxy + +After compiling and running, test by sending a request to `127.0.0.1:6191`: + +```bash +curl -v http://127.0.0.1:6191/ +``` + +1. The proxy connects to `ip.jsontest.com` at `142.251.2.121:80`. +2. It receives a JSON body similar to: + ```json + { + "ip": "123.123.123.123" + } + ``` +3. The proxy transforms this JSON into YAML, e.g.: + ```yaml + ip: 123.123.123.123 + ``` +4. The client sees the **YAML** response instead of JSON. + +--- + +## Modifying the Code + +### Different Upstream Host + +- Change `HOST` from `"ip.jsontest.com"` to your desired server domain. +- Adjust `addr` to the correct IP/port, or replace `HttpPeer::new(self.addr, false, HOST.to_owned())` with DNS resolution logic. + +### Other Transformations (XML, CSV, etc.) + +- Instead of `serde_yaml`, you could parse the JSON data into a struct, then serialize to **XML** (using `serde_xml_rs`) or **CSV** (using `csv`). +- The principle remains the same: **accumulate** body data, **convert** it, then **replace** the final body. + +### Body Streaming vs. Buffering + +- This example **buffers** the entire body to parse JSON in one go. For large bodies, you might prefer a streaming parser approach. +- You’d parse each chunk as it arrives, but that may get more complex. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; + +const HOST: &str = "ip.jsontest.com"; + +#[derive(Serialize, Deserialize)] +pub struct Resp { + ip: String, +} + +pub struct Json2Yaml { + addr: std::net::SocketAddr, +} + +pub struct MyCtx { + buffer: Vec, +} + +#[async_trait] +impl ProxyHttp for Json2Yaml { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { buffer: vec![] } + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let peer = Box::new(HttpPeer::new(self.addr, false, HOST.to_owned())); + Ok(peer) + } + + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", HOST) + .unwrap(); + Ok(()) + } + + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + // Remove Content-Length because the size of the new body is unknown + upstream_response.remove_header("Content-Length"); + upstream_response + .insert_header("Transfer-Encoding", "chunked") + .unwrap(); + Ok(()) + } + + fn response_body_filter( + &self, + _session: &mut Session, + body: &mut Option, + end_of_stream: bool, + ctx: &mut Self::CTX, + ) -> Result> + where + Self::CTX: Send + Sync, + { + // Buffer the data + if let Some(b) = body.take() { + ctx.buffer.extend_from_slice(&b); + } + + if end_of_stream { + // This is the last chunk; we can process the data now + let json_body: Resp = serde_json::from_slice(&ctx.buffer).unwrap(); + let yaml_body = serde_yaml::to_string(&json_body).unwrap(); + *body = Some(Bytes::copy_from_slice(yaml_body.as_bytes())); + } + + Ok(None) + } +} + +fn main() { + env_logger::init(); + + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + Json2Yaml { + addr: "142.251.2.121:80".parse().unwrap(), + }, + ); + + my_proxy.add_tcp("127.0.0.1:6191"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +--- + +## Conclusion + +This **Json2Yaml** example highlights how Pingora allows you to **reshape** the response body in real-time—useful for tasks like: + +- **Protocol translation** (JSON -> YAML, or even gRPC-web -> gRPC). +- **Content filtering or injection** (e.g., removing sensitive data). +- **Logging or analytics** (inspecting the body before proxying). + +With a few simple steps—removing the `Content-Length`, setting chunked encoding, and hooking into Pingora’s `response_body_filter`—you can achieve complex transformations in a lightweight, asynchronous Rust proxy. diff --git a/pingora_tutorial/multi_lb.md b/pingora_tutorial/multi_lb.md new file mode 100644 index 000000000..4b35c3e6a --- /dev/null +++ b/pingora_tutorial/multi_lb.md @@ -0,0 +1,315 @@ +# Tutorial: Routing Requests Across Multiple Load Balancer Clusters with Pingora + +This example demonstrates how to set up **multiple load-balancing clusters** in [Pingora](https://docs.rs/pingora-core/latest/pingora_core/) and then **route** incoming requests between them based on the request path. Specifically: + +- Requests whose paths start with **`/one/`** go to **`cluster_one`**. +- All other requests go to **`cluster_two`**. + +Each cluster has its own set of upstreams and **TCP health checks** to dynamically remove unhealthy servers from rotation. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Setup](#imports--setup) + - [`Router` Struct & `ProxyHttp` Implementation](#router-struct--proxyhttp-implementation) + 1. [Choosing the Right Cluster](#1-choosing-the-right-cluster) + - [Building Clusters with `build_cluster_service`](#building-clusters-with-build_cluster_service) + - [Main Function & Server Setup](#main-function--server-setup) +3. [Examples of Modifications](#examples-of-modifications) + 1. [Different Request Routing Logic](#1-different-request-routing-logic) + 2. [Custom Health Checks](#2-custom-health-checks) + 3. [TLS or Plain HTTP Upstreams](#3-tls-or-plain-http-upstreams) + 4. [Linking This to a Local Test Server](#4-linking-this-to-a-local-test-server) +4. [Complete Example Code](#complete-example-code) +5. [Testing & Observing Behavior](#testing--observing-behavior) +6. [Conclusion](#conclusion) + +--- + +## Introduction + +By **composing** multiple load-balanced clusters in Pingora, you can route traffic to different backend pools depending on HTTP properties—like path, headers, or methods. This is useful for splitting traffic between microservices, regions, or canary vs. production clusters. + +--- + +## Code Overview + +### Imports & Setup + +```rust +use async_trait::async_trait; +use std::sync::Arc; + +use pingora_core::{prelude::*, services::background::GenBackgroundService}; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_load_balancing::{ + health_check::TcpHealthCheck, + selection::{BackendIter, BackendSelection, RoundRobin}, + LoadBalancer, +}; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; +``` + +- **`RoundRobin`**: Our load-balancing strategy. +- **`TcpHealthCheck`**: A basic health check that attempts a TCP connection. +- **`LoadBalancer`**: The structure that manages a set of backends. +- **`GenBackgroundService`**: Lets us run these health checks in the background. + +### `Router` Struct & `ProxyHttp` Implementation + +```rust +struct Router { + cluster_one: Arc>, + cluster_two: Arc>, +} +``` + +- We store references to **two** separate `LoadBalancer` instances in `cluster_one` and `cluster_two`. + +#### 1. Choosing the Right Cluster + +```rust +#[async_trait] +impl ProxyHttp for Router { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // Determine LB cluster based on request URI + let cluster = if session.req_header().uri.path().starts_with("/one/") { + &self.cluster_one + } else { + &self.cluster_two + }; + + let upstream = cluster.select(b"", 256).unwrap(); + println!("upstream peer is: {:?}", upstream); + + let peer = Box::new(HttpPeer::new( + upstream, + true, // TLS = true + "one.one.one.one".to_string() // SNI host + )); + Ok(peer) + } +} +``` + +- **`upstream_peer`** picks the **cluster** based on whether the request path starts with `"/one/"`. Otherwise, it uses the second cluster. +- `cluster.select(b"", 256)` chooses an upstream from the load balancer, skipping unhealthy servers. +- We create an `HttpPeer` set to **TLS = `true`** with the SNI host `"one.one.one.one"` (placeholder example). + +### Building Clusters with `build_cluster_service` + +```rust +fn build_cluster_service(upstreams: &[&str]) -> GenBackgroundService> +where + S: BackendSelection + 'static, + S::Iter: BackendIter, +{ + let mut cluster = LoadBalancer::try_from_iter(upstreams).unwrap(); + cluster.set_health_check(TcpHealthCheck::new()); + cluster.health_check_frequency = Some(std::time::Duration::from_secs(1)); + + background_service("cluster health check", cluster) +} +``` + +1. We create a `LoadBalancer` from the provided upstream addresses (like `["1.1.1.1:443", "127.0.0.1:343"]`). +2. Attach a **TCP health check** with `set_health_check`. +3. Set the **`health_check_frequency`** to 1 second, meaning we test each backend’s health once per second. +4. **`background_service(...)`** returns a `GenBackgroundService>` that we’ll add to the Pingora server. This service runs health checks in the background. + +### Main Function & Server Setup + +```rust +fn main() { + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + // Build two clusters + let cluster_one = build_cluster_service::(&["1.1.1.1:443", "127.0.0.1:343"]); + let cluster_two = build_cluster_service::(&["1.0.0.1:443", "127.0.0.2:343"]); + + let router = Router { + cluster_one: cluster_one.task(), + cluster_two: cluster_two.task(), + }; + + let mut router_service = http_proxy_service(&my_server.configuration, router); + router_service.add_tcp("0.0.0.0:6188"); + + my_server.add_service(router_service); + my_server.add_service(cluster_one); + my_server.add_service(cluster_two); + + my_server.run_forever(); +} +``` + +1. **`build_cluster_service`** creates two separate clusters: + - **Cluster One**: `[ "1.1.1.1:443", "127.0.0.1:343" ]` + - **Cluster Two**: `[ "1.0.0.1:443", "127.0.0.2:343" ]` +2. Each cluster returns a background service. We attach them to the server so they can run health checks. +3. We create a `Router` that references **both** clusters. +4. **`router_service.add_tcp("0.0.0.0:6188")`** => The proxy listens on port 6188. +5. Finally, **`my_server.run_forever()`** starts the server. + +--- + +## Examples of Modifications + +### 1. Different Request Routing Logic + +- Instead of checking `.path().starts_with("/one/")`, you could: + - Check a **header** or **query param**. + - Look at the **HTTP method**. + - Use **regex** or more complex path matching (like `/api/v1/` vs `/api/v2/`). + +### 2. Custom Health Checks + +- By default, `TcpHealthCheck` just tests TCP connectivity. +- Use an **HTTP** health check if you need to confirm an endpoint is returning `200 OK`. +- Implement your own health check trait if your upstream requires a specialized approach. + +### 3. TLS or Plain HTTP Upstreams + +- In `HttpPeer::new(upstream, true, "one.one.one.one")`, the second param is `true` for **HTTPS**. Switch to `false` if your upstream is plain **HTTP**. +- Adjust the SNI host accordingly. + +### 4. Linking This to a Local Test Server + +- If you have a local server listening on `127.0.0.1:3000`, change the upstream strings to `"127.0.0.1:3000"`. +- You may also disable health checks or set `health_check_frequency` to a higher interval for testing. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; +use std::sync::Arc; + +use pingora_core::{prelude::*, services::background::GenBackgroundService}; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_load_balancing::{ + health_check::TcpHealthCheck, + selection::{BackendIter, BackendSelection, RoundRobin}, + LoadBalancer, +}; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; + +struct Router { + cluster_one: Arc>, + cluster_two: Arc>, +} + +#[async_trait] +impl ProxyHttp for Router { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let cluster = if session.req_header().uri.path().starts_with("/one/") { + &self.cluster_one + } else { + &self.cluster_two + }; + + let upstream = cluster.select(b"", 256).unwrap(); + println!("upstream peer is: {:?}", upstream); + + let peer = Box::new(HttpPeer::new( + upstream, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +fn build_cluster_service(upstreams: &[&str]) -> GenBackgroundService> +where + S: BackendSelection + 'static, + S::Iter: BackendIter, +{ + let mut cluster = LoadBalancer::try_from_iter(upstreams).unwrap(); + cluster.set_health_check(TcpHealthCheck::new()); + cluster.health_check_frequency = Some(std::time::Duration::from_secs(1)); + + background_service("cluster health check", cluster) +} + +fn main() { + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + // Two clusters, each with two upstreams + let cluster_one = build_cluster_service::(&["1.1.1.1:443", "127.0.0.1:343"]); + let cluster_two = build_cluster_service::(&["1.0.0.1:443", "127.0.0.2:343"]); + + let router = Router { + cluster_one: cluster_one.task(), + cluster_two: cluster_two.task(), + }; + let mut router_service = http_proxy_service(&my_server.configuration, router); + router_service.add_tcp("0.0.0.0:6188"); + + // Register the services (the router + background health checks) + my_server.add_service(router_service); + my_server.add_service(cluster_one); + my_server.add_service(cluster_two); + + my_server.run_forever(); +} +``` + +--- + +## Testing & Observing Behavior + +1. **Start the server**: + ```bash + cargo run + ``` + The proxy listens on `127.0.0.1:6188` (plaintext). + +2. **Send requests**: + - `curl 127.0.0.1:6188/one/` -> Goes to `cluster_one`. + - `curl 127.0.0.1:6188/two/` -> Goes to `cluster_two`. + +3. **Health checks**: + - Each cluster runs health checks on its upstreams every second. Unhealthy ones are removed. + - You’ll see in logs if certain backends (`127.0.0.1:343`) fail and are removed. + +4. **Logs**: + - Look for `println!("upstream peer is: {:?}", upstream);` to see which IP/port was selected. + +--- + +## Conclusion + +By creating **multiple LoadBalancer** instances and referencing them in a **Router** that implements `ProxyHttp`, you can easily **split traffic** among distinct sets of backends. This is a flexible pattern for: + +- **Versioned APIs**: e.g., `/v1/` vs. `/v2/`. +- **Geo-based routing**: choose the nearest cluster to the user. +- **Canary releases**: route a fraction of paths to a new server for testing. + +Take this example further by customizing your route logic, adding advanced health checks, or using different load-balancing strategies. Happy coding! diff --git a/pingora_tutorial/rate_limiter.md b/pingora_tutorial/rate_limiter.md new file mode 100644 index 000000000..eded2c000 --- /dev/null +++ b/pingora_tutorial/rate_limiter.md @@ -0,0 +1,486 @@ +# Tutorial: Building a Rate-Limited, Load-Balanced HTTP Proxy in Rust with Pingora + +This tutorial shows how to create a **load-balanced** and **rate-limited** HTTP proxy using the [Pingora](https://docs.rs/pingora-core/latest/pingora_core/) ecosystem in Rust. We’ll break down the code step by step, explaining how the **load balancer**, **health checks**, and **rate limiting** are configured. We’ll also discuss how you can modify or extend each component for different scenarios (multiple upstreams, custom health checks, varying rate limits, etc.). + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Core Setup](#imports--core-setup) + - [Main Function](#main-function) + 1. [Creating the Pingora `Server`](#1-creating-the-pingora-server) + 2. [Setting Up the Load Balancer](#2-setting-up-the-load-balancer) + - [Load Balancer Construction](#load-balancer-construction) + - [Health Check Configuration](#health-check-configuration) + 3. [Background Service & Final Service Registration](#3-background-service--final-service-registration) + - [`LB` Struct and `ProxyHttp` Implementation](#lb-struct-and-proxyhttp-implementation) + 1. [Rate Limiting with `RATE_LIMITER`](#1-rate-limiting-with-rate_limiter) + 2. [Request Filter & Limit Enforcement](#2-request-filter--limit-enforcement) + 3. [Upstream Selection](#3-upstream-selection) + 4. [Upstream Request Filter](#4-upstream-request-filter) +3. [Examples of Customizations](#examples-of-customizations) + 1. [Changing the Rate Limit Policy](#1-changing-the-rate-limit-policy) + 2. [Multiple or Dynamic Upstreams](#2-multiple-or-dynamic-upstreams) + 3. [Custom Health Checks](#3-custom-health-checks) + 4. [TLS Configuration & SNI](#4-tls-configuration--sni) + 5. [Additional Filters & Logging](#5-additional-filters--logging) +4. [Complete Example Code](#complete-example-code) +5. [Testing & Verification](#testing--verification) +6. [Conclusion](#conclusion) + +--- + +## Introduction + +In a **modern proxy** setup, you might need to: + +- **Distribute** traffic among multiple upstream servers. +- **Check** the health of your servers and only send traffic to healthy endpoints. +- **Rate-limit** certain clients, so they don’t overwhelm your service. + +This code uses **Pingora**’s load balancing and rate-limiting modules to demonstrate all these capabilities. + +--- + +## Code Overview + +Below is an annotated version of the code. Read on to see how the pieces fit together, and skip to “[Examples of Customizations](#examples-of-customizations)” if you want practical tips for adapting this code to various use cases. + +### Imports & Core Setup + +```rust +use async_trait::async_trait; +use once_cell::sync::Lazy; +use pingora_core::prelude::*; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_limits::rate::Rate; +use pingora_load_balancing::prelude::{RoundRobin, TcpHealthCheck}; +use pingora_load_balancing::LoadBalancer; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; +use std::sync::Arc; +use std::time::Duration; +``` + +- **`pingora_core::prelude::*`**: Brings common Pingora items like `Server`, `Result`, etc. into scope. +- **`pingora_http`**: For dealing with request/response headers. +- **`pingora_limits::rate::Rate`**: The rate-limiting mechanism we’ll use. +- **`pingora_load_balancing`**: Contains load-balancing strategies (e.g. `RoundRobin`) and health checks (e.g. `TcpHealthCheck`). +- **`pingora_proxy`**: Where `ProxyHttp` (the main trait for handling proxy logic) and `http_proxy_service` live. +- **`once_cell::sync::Lazy`**: Lets us lazily initialize static data (e.g. the rate limiter). +- **`std::sync::Arc`**: Allows shared ownership of data (like our `LoadBalancer`). +- **`std::time::Duration`**: For specifying time intervals (e.g. the health check frequency). + +### Main Function + +```rust +fn main() { + env_logger::init(); + + let mut server = Server::new(None).unwrap(); + server.bootstrap(); + + let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443"]).unwrap(); + + // Set health check + let hc = TcpHealthCheck::new(); + upstreams.set_health_check(hc); + upstreams.health_check_frequency = Some(Duration::from_secs(1)); + + // Set background service + let background = background_service("health check", upstreams); + let upstreams = background.task(); + + // Set load balancer + let mut lb = http_proxy_service(&server.configuration, LB(upstreams)); + lb.add_tcp("0.0.0.0:6188"); + + server.add_service(background); + server.add_service(lb); + server.run_forever(); +} +``` + +#### 1. Creating the Pingora `Server` + +```rust +let mut server = Server::new(None).unwrap(); +server.bootstrap(); +``` + +- We create a `Server` (with no custom configuration) and **bootstrap** it. This typically prepares internal tasks and resources. + +#### 2. Setting Up the Load Balancer + +##### Load Balancer Construction + +```rust +let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443"]).unwrap(); +``` + +- We create a **`LoadBalancer`** from a list of upstream addresses. Here, we use DNS resolvers for `1.1.1.1:443` and `1.0.0.1:443` (Cloudflare DNS addresses). +- The default policy is **`RoundRobin`**, meaning new requests alternate between these servers. + +##### Health Check Configuration + +```rust +let hc = TcpHealthCheck::new(); +upstreams.set_health_check(hc); +upstreams.health_check_frequency = Some(Duration::from_secs(1)); +``` + +- We use a **TCP health check**, which tries to connect to the upstream. +- `health_check_frequency` specifies how often Pingora runs these checks. If a server fails, it’s temporarily removed from rotation until it’s healthy again. + +#### 3. Background Service & Final Service Registration + +```rust +let background = background_service("health check", upstreams); +let upstreams = background.task(); +``` + +- We wrap the `LoadBalancer` in a “background service,” allowing Pingora to regularly run the health checks. +- `background.task()` returns an `Arc>` that we can use in our `LB` struct. + +```rust +let mut lb = http_proxy_service(&server.configuration, LB(upstreams)); +lb.add_tcp("0.0.0.0:6188"); + +server.add_service(background); +server.add_service(lb); +server.run_forever(); +``` + +- We create an **HTTP proxy service** using our `LB` struct, listening on port `6188`. +- We add both the background service (for health checks) and the `lb` proxy service to our `server`. +- `run_forever()` blocks the main thread and starts the server loop. + +--- + +## `LB` Struct and `ProxyHttp` Implementation + +```rust +pub struct LB(Arc>); +``` + +- **`LB`** wraps our Arc-ed load balancer. We’ll implement `ProxyHttp` on it. + +### 1. Rate Limiting with `RATE_LIMITER` + +```rust +static RATE_LIMITER: Lazy = Lazy::new(|| Rate::new(Duration::from_secs(1))); +const MAX_REQ_PER_SEC: isize = 1; +``` + +- **`RATE_LIMITER`**: A static instance of Pingora’s `Rate` limiter, configured with a **1-second** window. +- **`MAX_REQ_PER_SEC`**: The maximum number of requests allowed per **appid** per second. + +### 2. Request Filter & Limit Enforcement + +```rust +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result { + let appid = match self.get_request_appid(session) { + None => return Ok(false), // No client appid found, skip rate limiting + Some(appid) => appid, + }; + + let curr_window_requests = RATE_LIMITER.observe(&appid, 1); + if curr_window_requests > MAX_REQ_PER_SEC { + // Rate limited, return 429 + let mut header = ResponseHeader::build(429, None).unwrap(); + header + .insert_header("X-Rate-Limit-Limit", MAX_REQ_PER_SEC.to_string()) + .unwrap(); + header + .insert_header("X-Rate-Limit-Remaining", "0") + .unwrap(); + header.insert_header("X-Rate-Limit-Reset", "1").unwrap(); + session.set_keepalive(None); + session + .write_response_header(Box::new(header), true) + .await?; + return Ok(true); + } + Ok(false) + } + // ... +} +``` + +- **`get_request_appid`**: A helper method that reads an `appid` header from the client request. This is how we track which “client” is sending the request. +- If `appid` is absent, **rate limiting is skipped**. (You might want to handle it differently—see [Customizations](#examples-of-customizations) for ideas.) +- If the `appid` has exceeded `MAX_REQ_PER_SEC`, we respond with **HTTP 429** (“Too Many Requests”) and add some custom rate limit headers to inform the client about the limit. +- Returning `Ok(true)` means “**I’ve already sent a response; stop processing**.” + +### 3. Upstream Selection + +```rust +async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result> { + let upstream = self.0.select(b"", 256).unwrap(); + let peer = Box::new(HttpPeer::new( + upstream, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) +} +``` + +- We call `self.0.select(b"", 256).unwrap()` on our load balancer to pick an upstream (using the RoundRobin strategy). + - `b""` is a placeholder for a key that can be used for consistent hashing or other load balancing strategies. + - `256` is the maximum number of tries (to skip unhealthy servers). +- **`HttpPeer::new(..., true, "one.one.one.one")`** indicates **TLS** is enabled and sets `"one.one.one.one"` as the SNI hostname. +- You could easily swap this out for your own domain or disable TLS (`false`) for a plain HTTP backend. + +### 4. Upstream Request Filter + +```rust +async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, +) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) +} +``` + +- Before sending the request to the upstream, we set the `Host` header to `"one.one.one.one"`. + - This is important if the upstream server expects a specific `Host` for routing or TLS handshake. +- You can modify or add other headers here, too. + +--- + +## Examples of Customizations + +### 1. Changing the Rate Limit Policy + +- **Different Window**: Change `Duration::from_secs(1)` to `Duration::from_secs(60)` if you want a per-minute limit. +- **Per-IP or Per-Header**: Instead of an `appid`, you can track the client’s IP from `X-Forwarded-For` or `session.remote_addr()` for rate limiting. +- **Adjust the Limit**: Change `MAX_REQ_PER_SEC` to a higher value or make it dynamic based on the `appid`: + + ```rust + fn get_client_limit(appid: &str) -> isize { + match appid { + "premium_user" => 10, + _ => 1, + } + } + ``` + +### 2. Multiple or Dynamic Upstreams + +- **Add More Servers**: `["1.1.1.1:443", "1.0.0.1:443", "8.8.8.8:443"]`. +- **Different Load Balancing Strategies**: Pingora also supports Weighted Round Robin, Least Connections, Consistent Hash, etc. +- **Runtime Changes**: If you need to add or remove upstreams on the fly, you can explore Pingora’s dynamic reconfiguration features. + +### 3. Custom Health Checks + +- **HTTP Health Check**: Instead of `TcpHealthCheck`, you can define a custom module that sends an HTTP GET to a specific endpoint (e.g. `/health`). +- **Interval**: Change `health_check_frequency` to run less (like `Duration::from_secs(5)`) or more often. + +### 4. TLS Configuration & SNI + +- **Disable TLS**: Change the second argument of `HttpPeer::new(...)` to `false`. +- **Custom Hostname**: Instead of `"one.one.one.one".to_string()`, use your actual domain, e.g. `"api.example.com"`. +- **Cert Validation**: For internal or dev environments, you might skip certificate validation or specify custom root CAs. This typically requires additional Pingora or Rust TLS configuration. + +### 5. Additional Filters & Logging + +- **Request & Response Filters**: Add more methods (e.g. `response_filter`) to transform or log the upstream response (inserting, removing, or modifying headers). +- **Body Filters**: For advanced transformations, you can process request or response bodies asynchronously. +- **Logging**: Override `logging(&self, session, error, ctx)` from `ProxyHttp` to record request metrics, response codes, or custom logs. + +--- + +## Complete Example Code + +```rust +use async_trait::async_trait; +use once_cell::sync::Lazy; +use pingora_core::prelude::*; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_limits::rate::Rate; +use pingora_load_balancing::prelude::{RoundRobin, TcpHealthCheck}; +use pingora_load_balancing::LoadBalancer; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; +use std::sync::Arc; +use std::time::Duration; + +fn main() { + env_logger::init(); + + let mut server = Server::new(None).unwrap(); + server.bootstrap(); + + // Create a load balancer with two upstream servers + let mut upstreams = LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443"]).unwrap(); + + // Configure health checks + let hc = TcpHealthCheck::new(); + upstreams.set_health_check(hc); + // Check health every 1 second + upstreams.health_check_frequency = Some(Duration::from_secs(1)); + + // Create a background service to manage health checks + let background = background_service("health check", upstreams); + let upstreams = background.task(); + + // Create the proxy service that uses LB + let mut lb = http_proxy_service(&server.configuration, LB(upstreams)); + lb.add_tcp("0.0.0.0:6188"); + + // Register services (the background service + the load-balanced proxy) + server.add_service(background); + server.add_service(lb); + server.run_forever(); +} + +// A struct that holds the load balancer inside an Arc +pub struct LB(Arc>); + +impl LB { + pub fn get_request_appid(&self, session: &Session) -> Option { + session + .req_header() + .headers + .get("appid") + .and_then(|v| v.to_str().ok().map(|s| s.to_string())) + } +} + +// Rate limit: 1 request/second per 'appid' +static RATE_LIMITER: Lazy = Lazy::new(|| Rate::new(Duration::from_secs(1))); +const MAX_REQ_PER_SEC: isize = 1; + +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + // Rate-limit requests based on "appid" header + async fn request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result { + let appid = match self.get_request_appid(session) { + None => return Ok(false), // No client ID found, skip rate limiting + Some(appid) => appid, + }; + + let curr_window_requests = RATE_LIMITER.observe(&appid, 1); + if curr_window_requests > MAX_REQ_PER_SEC { + let mut header = ResponseHeader::build(429, None).unwrap(); + header + .insert_header("X-Rate-Limit-Limit", MAX_REQ_PER_SEC.to_string()) + .unwrap(); + header + .insert_header("X-Rate-Limit-Remaining", "0") + .unwrap(); + header.insert_header("X-Rate-Limit-Reset", "1").unwrap(); + session.set_keepalive(None); + session + .write_response_header(Box::new(header), true) + .await?; + // Returning Ok(true) means we've already responded + return Ok(true); + } + Ok(false) + } + + // Select an upstream using round-robin + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let upstream = self.0.select(b"", 256).unwrap(); + let peer = Box::new(HttpPeer::new( + upstream, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } + + // Insert/modify headers before forwarding to the upstream + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) + } +} +``` + +--- + +## Testing & Verification + +1. **Build and Run**: + ```bash + cargo run + ``` + The server starts, listening on port `6188` for incoming traffic. + +2. **Check Health**: Every second, Pingora attempts a TCP connection to `1.1.1.1:443` and `1.0.0.1:443` to verify they are online. If one fails, it’s removed from the rotation. + +3. **Rate Limit Test**: + ```bash + # Without 'appid' header: no rate limit + curl -v http://localhost:6188/ + + # With 'appid: myclient', first request is allowed: + curl -v http://localhost:6188/ -H "appid: myclient" + + # Immediately send another with the same 'appid': + curl -v http://localhost:6188/ -H "appid: myclient" + # If quick enough, you should get "429 Too Many Requests" + ``` + +4. **Load Balancing**: If both upstreams are healthy, your requests should alternate between `1.1.1.1` and `1.0.0.1`. You can monitor logs or add your own logging to confirm. + +--- + +## Conclusion + +This example shows how to **rate-limit** requests, **health check** your upstream servers, and **distribute** traffic among multiple backends. By modifying the code in the suggested ways, you can: + +- Implement more **complex rate limits** (e.g., multiple tiers, request bursts). +- Choose **different load balancing** strategies (weighted round robin, consistent hash). +- Customize or replace the **health checks**. +- Inject or remove HTTP headers **before** sending the request upstream. +- Integrate additional **request/response filters** for logging, authentication, rewriting, or other functionalities. + +Happy coding! With Pingora, you have a flexible toolset to build production-ready proxies tailored to your application’s needs. diff --git a/pingora_tutorial/src/ctx.rs b/pingora_tutorial/src/ctx.rs new file mode 100644 index 000000000..fd282c92f --- /dev/null +++ b/pingora_tutorial/src/ctx.rs @@ -0,0 +1,91 @@ +use async_trait::async_trait; +use log::info; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use pingora_core::server::configuration::Opt; +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::RequestHeader; +use pingora_proxy::{ProxyHttp, Session}; + +/// Global request counter using `AtomicUsize` for thread-safe atomic operations +static REQ_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub struct MyProxy { + /// Counter for beta users + beta_counter: AtomicUsize, +} + +pub struct MyCtx { + beta_user: bool, +} + +fn check_beta_user(req: &RequestHeader) -> bool { + // Simple logic to check if user is beta + req.headers.get("beta-flag").is_some() +} + +#[async_trait] +impl ProxyHttp for MyProxy { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { beta_user: false } + } + + async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result { + ctx.beta_user = check_beta_user(session.req_header()); + Ok(false) // Continue processing the request + } + + async fn upstream_peer( + &self, + _session: &mut Session, + ctx: &mut Self::CTX, + ) -> Result> { + // Increment the global request counter atomically + let req_counter = REQ_COUNTER.fetch_add(1, Ordering::Relaxed) + 1; + + let addr = if ctx.beta_user { + // Increment the beta user counter atomically + let beta_count = self.beta_counter.fetch_add(1, Ordering::Relaxed) + 1; + info!("I'm a beta user #{beta_count}"); + ("1.0.0.1", 443) + } else { + info!("I'm a user #{req_counter}"); + ("1.1.1.1", 443) + }; + + let peer = Box::new(HttpPeer::new( + addr, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +// To run the example: +// RUST_LOG=INFO cargo run --example ctx +// curl 127.0.0.1:6190 -H "Host: one.one.one.one" +// curl 127.0.0.1:6190 -H "Host: one.one.one.one" -H "beta-flag: 1" +fn main() { + env_logger::init(); + + // Read command line arguments + let opt = Opt::parse_args(); + let mut my_server = Server::new(Some(opt)).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyProxy { + beta_counter: AtomicUsize::new(0), + }, + ); + my_proxy.add_tcp("0.0.0.0:6190"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/gateway.rs b/pingora_tutorial/src/gateway.rs new file mode 100644 index 000000000..de02c8ef3 --- /dev/null +++ b/pingora_tutorial/src/gateway.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use log::info; +use prometheus::{IntCounter, register_int_counter}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; + +fn check_login(req: &RequestHeader) -> bool { + req.headers + .get("Authorization") + .map(|v| v.as_bytes() == b"password") + .unwrap_or(false) +} + +pub struct MyGateway { + req_metric: IntCounter, +} + +#[async_trait] +impl ProxyHttp for MyGateway { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + // Decide whether to short-circuit this request (e.g. returning an error) + // before even forwarding to the upstream. + async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { + // Check if the user is authorized for /login + if session.req_header().uri.path().starts_with("/login") && !check_login(session.req_header()) { + let _ = session.respond_error(403).await; + return Ok(true); // Stop further processing + } + Ok(false) + } + + // Choose the upstream server. + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // Suppose your local server is listening on 127.0.0.1:3000, plain HTTP: + let addr = ("127.0.0.1", 3000); + info!("Connecting to {:?}", addr); + + // The second param is `false` if it's HTTP, `true` if it's HTTPS. + // The third param is used for SNI if HTTPS. If HTTP, it's typically a placeholder. + let peer = Box::new(HttpPeer::new(addr, false, "127.0.0.1".to_string())); + Ok(peer) + } + + // After receiving a response from the upstream, optionally modify headers/status. + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_response + .insert_header("Server", "MyGateway") + .unwrap(); + upstream_response.remove_header("alt-svc"); + Ok(()) + } + + // Called after we finish handling the request, for logging or additional metrics. + async fn logging( + &self, + session: &mut Session, + _e: Option<&pingora_core::Error>, + _ctx: &mut Self::CTX, + ) { + let response_code = session + .response_written() + .map_or(0, |resp| resp.status.as_u16()); + + info!( + "Request to {} responded with status code {}", + session.req_header().uri.path(), + response_code + ); + + // Prometheus counter + self.req_metric.inc(); + } +} + +fn main() { + env_logger::init(); + + // Create the server without options + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let req_metric = register_int_counter!("req_counter", "Number of requests").unwrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + MyGateway { + req_metric, + }, + ); + // This is where the gateway listens for incoming traffic + my_proxy.add_tcp("0.0.0.0:6191"); + my_server.add_service(my_proxy); + + // Prometheus metrics service on port 6192 + let mut prometheus_service_http = + pingora_core::services::listening::Service::prometheus_http_service(); + prometheus_service_http.add_tcp("127.0.0.1:6192"); + my_server.add_service(prometheus_service_http); + + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/grpc_web_modules.rs b/pingora_tutorial/src/grpc_web_modules.rs new file mode 100644 index 000000000..e72a6bf23 --- /dev/null +++ b/pingora_tutorial/src/grpc_web_modules.rs @@ -0,0 +1,76 @@ +use async_trait::async_trait; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + modules::http::{ + grpc_web::{GrpcWeb, GrpcWebBridge}, + HttpModules, + }, +}; +use pingora_proxy::{ProxyHttp, Session}; + +/// This example shows how to use the gRPC-web bridge module + +pub struct GrpcWebBridgeProxy; + +#[async_trait] +impl ProxyHttp for GrpcWebBridgeProxy { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + fn init_downstream_modules(&self, modules: &mut HttpModules) { + // Add the gRPC web module + modules.add_module(Box::new(GrpcWeb)) + } + + async fn early_request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result<()> { + let grpc = session + .downstream_modules_ctx + .get_mut::() + .expect("GrpcWebBridge module added"); + + // Initialize gRPC module for this request + grpc.init(); + Ok(()) + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // This needs to be your gRPC server + let grpc_peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(grpc_peer) + } +} + +// RUST_LOG=INFO cargo run --example grpc_web_module + +fn main() { + env_logger::init(); + + // Create server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = + pingora_proxy::http_proxy_service(&my_server.configuration, GrpcWebBridgeProxy); + my_proxy.add_tcp("0.0.0.0:6194"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/load_loadbalancer.rs b/pingora_tutorial/src/load_loadbalancer.rs new file mode 100644 index 000000000..943451942 --- /dev/null +++ b/pingora_tutorial/src/load_loadbalancer.rs @@ -0,0 +1,85 @@ +use async_trait::async_trait; +use log::info; +use pingora_core::services::background::background_service; +use std::{sync::Arc, time::Duration}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_core::{ + listeners::tls::TlsSettings, +}; +use pingora_load_balancing::{health_check, selection::RoundRobin, LoadBalancer}; +use pingora_proxy::{ProxyHttp, Session}; +use pingora_http::RequestHeader; + +pub struct LB(Arc>); + +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result> { + let upstream = self + .0 + .select(b"", 256) // hash doesn't matter + .unwrap(); + + info!("upstream peer is: {:?}", upstream); + + let peer = Box::new(HttpPeer::new(upstream, true, "one.one.one.one".to_string())); + Ok(peer) + } + + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) + } +} + +// RUST_LOG=INFO cargo run --example load_balancer +fn main() { + env_logger::init(); + + // Create the server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + // "127.0.0.1:343" is just a bad server + let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443", "127.0.0.1:343"]).unwrap(); + + // We add health check in the background so that the bad server is never selected. + let hc = health_check::TcpHealthCheck::new(); + upstreams.set_health_check(hc); + upstreams.health_check_frequency = Some(Duration::from_secs(1)); + + let background = background_service("health check", upstreams); + + let upstreams = background.task(); + + let mut lb = pingora_proxy::http_proxy_service(&my_server.configuration, LB(upstreams)); + lb.add_tcp("0.0.0.0:6188"); + + let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); + let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); + + let mut tls_settings = TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + tls_settings.enable_h2(); + lb.add_tls_with_settings("0.0.0.0:6189", None, tls_settings); + + my_server.add_service(lb); + my_server.add_service(background); + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/modify_response.rs b/pingora_tutorial/src/modify_response.rs new file mode 100644 index 000000000..0be9edbe8 --- /dev/null +++ b/pingora_tutorial/src/modify_response.rs @@ -0,0 +1,112 @@ +use async_trait::async_trait; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session}; + +const HOST: &str = "ip.jsontest.com"; + +#[derive(Serialize, Deserialize)] +pub struct Resp { + ip: String, +} + +pub struct Json2Yaml { + addr: std::net::SocketAddr, +} + +pub struct MyCtx { + buffer: Vec, +} + +#[async_trait] +impl ProxyHttp for Json2Yaml { + type CTX = MyCtx; + + fn new_ctx(&self) -> Self::CTX { + MyCtx { buffer: vec![] } + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let peer = Box::new(HttpPeer::new(self.addr, false, HOST.to_owned())); + Ok(peer) + } + + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", HOST) + .unwrap(); + Ok(()) + } + + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + // Remove Content-Length because the size of the new body is unknown + upstream_response.remove_header("Content-Length"); + upstream_response + .insert_header("Transfer-Encoding", "chunked") + .unwrap(); + Ok(()) + } + + fn response_body_filter( + &self, + _session: &mut Session, + body: &mut Option, + end_of_stream: bool, + ctx: &mut Self::CTX, + ) -> Result> + where + Self::CTX: Send + Sync, + { + // Buffer the data + if let Some(b) = body.take() { + ctx.buffer.extend_from_slice(&b); + } + + if end_of_stream { + // This is the last chunk; we can process the data now + let json_body: Resp = serde_json::from_slice(&ctx.buffer).unwrap(); + let yaml_body = serde_yaml::to_string(&json_body).unwrap(); + *body = Some(Bytes::copy_from_slice(yaml_body.as_bytes())); + } + + Ok(None) + } +} + +fn main() { + env_logger::init(); + + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service( + &my_server.configuration, + Json2Yaml { + addr: "142.251.2.121:80".parse().unwrap(), + }, + ); + + my_proxy.add_tcp("127.0.0.1:6191"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/multi_lb.rs b/pingora_tutorial/src/multi_lb.rs new file mode 100644 index 000000000..599e71b63 --- /dev/null +++ b/pingora_tutorial/src/multi_lb.rs @@ -0,0 +1,91 @@ +use async_trait::async_trait; +use std::sync::Arc; + +use pingora_core::{prelude::*, services::background::GenBackgroundService}; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_load_balancing::{ + health_check::TcpHealthCheck, + selection::{BackendIter, BackendSelection, RoundRobin}, + LoadBalancer, +}; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; + +struct Router { + cluster_one: Arc>, + cluster_two: Arc>, +} + +#[async_trait] +impl ProxyHttp for Router { + type CTX = (); + + // Corrected the `new_ctx` method to return `Self::CTX` + fn new_ctx(&self) -> Self::CTX { + () + } + + // Updated the return type to `Result>` + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + // Determine LB cluster based on request URI + let cluster = if session.req_header().uri.path().starts_with("/one/") { + &self.cluster_one + } else { + &self.cluster_two + }; + + let upstream = cluster + .select(b"", 256) // Hash doesn't matter for round robin + .unwrap(); + + println!("upstream peer is: {:?}", upstream); + + // Create a new HttpPeer instance and wrap it in a Box + let peer = Box::new(HttpPeer::new( + upstream, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +fn build_cluster_service(upstreams: &[&str]) -> GenBackgroundService> +where + S: BackendSelection + 'static, + S::Iter: BackendIter, +{ + let mut cluster = LoadBalancer::try_from_iter(upstreams).unwrap(); + cluster.set_health_check(TcpHealthCheck::new()); + cluster.health_check_frequency = Some(std::time::Duration::from_secs(1)); + + background_service("cluster health check", cluster) +} + +// RUST_LOG=INFO cargo run --example multi_lb +// curl 127.0.0.1:6188/one/ +// curl 127.0.0.1:6188/two/ +fn main() { + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + // Build multiple clusters + let cluster_one = build_cluster_service::(&["1.1.1.1:443", "127.0.0.1:343"]); + let cluster_two = build_cluster_service::(&["1.0.0.1:443", "127.0.0.2:343"]); + + let router = Router { + cluster_one: cluster_one.task(), + cluster_two: cluster_two.task(), + }; + let mut router_service = http_proxy_service(&my_server.configuration, router); + router_service.add_tcp("0.0.0.0:6188"); + + my_server.add_service(router_service); + my_server.add_service(cluster_one); + my_server.add_service(cluster_two); + + my_server.run_forever(); +} diff --git a/pingora_tutorial/src/rate_limiter.rs b/pingora_tutorial/src/rate_limiter.rs new file mode 100644 index 000000000..14e3160c1 --- /dev/null +++ b/pingora_tutorial/src/rate_limiter.rs @@ -0,0 +1,122 @@ +use async_trait::async_trait; +use once_cell::sync::Lazy; +use pingora_core::prelude::*; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_limits::rate::Rate; +use pingora_load_balancing::prelude::{RoundRobin, TcpHealthCheck}; +use pingora_load_balancing::LoadBalancer; +use pingora_proxy::{http_proxy_service, ProxyHttp, Session}; +use std::sync::Arc; +use std::time::Duration; + +fn main() { + env_logger::init(); + + let mut server = Server::new(None).unwrap(); + server.bootstrap(); + + let mut upstreams = + LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443"]).unwrap(); + + // Set health check + let hc = TcpHealthCheck::new(); + upstreams.set_health_check(hc); + upstreams.health_check_frequency = Some(Duration::from_secs(1)); + + // Set background service + let background = background_service("health check", upstreams); + let upstreams = background.task(); + + // Set load balancer + let mut lb = http_proxy_service(&server.configuration, LB(upstreams)); + lb.add_tcp("0.0.0.0:6188"); + + server.add_service(background); + server.add_service(lb); + server.run_forever(); +} + +pub struct LB(Arc>); + +impl LB { + pub fn get_request_appid(&self, session: &Session) -> Option { + session + .req_header() + .headers + .get("appid") + .and_then(|v| v.to_str().ok().map(|s| s.to_string())) + } +} + +// Rate limiter +static RATE_LIMITER: Lazy = Lazy::new(|| Rate::new(Duration::from_secs(1))); + +// Max requests per second per client +const MAX_REQ_PER_SEC: isize = 1; + +#[async_trait] +impl ProxyHttp for LB { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + async fn request_filter( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result { + let appid = match self.get_request_appid(session) { + None => return Ok(false), // No client appid found, skip rate limiting + Some(appid) => appid, + }; + + // Retrieve the current window requests + let curr_window_requests = RATE_LIMITER.observe(&appid, 1); + if curr_window_requests > MAX_REQ_PER_SEC { + // Rate limited, return 429 + let mut header = ResponseHeader::build(429, None).unwrap(); + header + .insert_header("X-Rate-Limit-Limit", MAX_REQ_PER_SEC.to_string()) + .unwrap(); + header + .insert_header("X-Rate-Limit-Remaining", "0") + .unwrap(); + header.insert_header("X-Rate-Limit-Reset", "1").unwrap(); + session.set_keepalive(None); + session + .write_response_header(Box::new(header), true) + .await?; + return Ok(true); + } + Ok(false) + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let upstream = self.0.select(b"", 256).unwrap(); + // Set SNI + let peer = Box::new(HttpPeer::new( + upstream, + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } + + async fn upstream_request_filter( + &self, + _session: &mut Session, + upstream_request: &mut RequestHeader, + _ctx: &mut Self::CTX, + ) -> Result<()> { + upstream_request + .insert_header("Host", "one.one.one.one") + .unwrap(); + Ok(()) + } +} diff --git a/pingora_tutorial/src/test_server.rs b/pingora_tutorial/src/test_server.rs new file mode 100644 index 000000000..3ee544b3e --- /dev/null +++ b/pingora_tutorial/src/test_server.rs @@ -0,0 +1,45 @@ +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; + +// Function that starts the test web server +pub fn start_test_server(address: &str) -> std::io::Result<()> { + let listener = TcpListener::bind(address)?; + println!("Test server running on {}", address); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(move || { + handle_test_client(stream); + }); + } + Err(e) => eprintln!("Failed to accept connection: {}", e), + } + } + Ok(()) +} + +// Function to handle each client and respond with a "Hello. You made it!" page +fn handle_test_client(mut stream: TcpStream) { + let mut buffer = [0; 512]; + if stream.read(&mut buffer).is_ok() { + // HTTP response with "Hello. You made it!" message + let response = r#"HTTP/1.1 200 OK +Content-Type: text/html + + + +Welcome + +

Hello. You made it!

+ +"#; + + // Write the response back to the client + if stream.write_all(response.as_bytes()).is_err() { + eprintln!("Failed to send response"); + } + } +} + \ No newline at end of file diff --git a/pingora_tutorial/src/use_module.rs b/pingora_tutorial/src/use_module.rs new file mode 100644 index 000000000..388147338 --- /dev/null +++ b/pingora_tutorial/src/use_module.rs @@ -0,0 +1,120 @@ +use async_trait::async_trait; + +use pingora_core::modules::http::HttpModules; +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::RequestHeader; +use pingora_proxy::{ProxyHttp, Session}; +use std::any::Any; + +/// This example shows how to build and import 3rd party modules + +/// A simple ACL to check "Authorization: basic $credential" header +mod my_acl { + use super::*; + use pingora_core::modules::http::{HttpModule, HttpModuleBuilder, Module}; + use pingora_error::{Error, ErrorType}; // Import Error and ErrorType here + + // This is the struct for per-request module context + pub struct MyAclCtx { + credential_header: String, + } + + // Implement how the module would consume and/or modify request and/or response + #[async_trait] + impl HttpModule for MyAclCtx { + async fn request_header_filter(&mut self, req: &mut RequestHeader) -> Result<()> { + if let Some(auth) = req.headers.get("Authorization") { + if auth.as_bytes() != self.credential_header.as_bytes() { + return Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, credential mismatch", + ); + } else { + Ok(()) + } + } else { + Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, no auth header", + ) + } + } + + // Boilerplate code for all modules + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + } + + // This is the singleton object which will be attached to the server + pub struct MyAcl { + pub credential: String, + } + + impl HttpModuleBuilder for MyAcl { + // This function defines how to create each context. This function is called when a new request arrives + fn init(&self) -> Module { + Box::new(MyAclCtx { + // Make it easier to compare header + // We could also store this value in MyAcl and use Arc to share it with every context. + credential_header: format!("basic {}", self.credential), + }) + } + } +} + +pub struct MyProxy; + +#[async_trait] +impl ProxyHttp for MyProxy { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + // This function is only called once when the server starts + fn init_downstream_modules(&self, modules: &mut HttpModules) { + // Add the module to MyProxy + modules.add_module(Box::new(my_acl::MyAcl { + credential: "testcode".into(), + })); + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +// To run the example: +// RUST_LOG=INFO cargo run --example use_module +// curl 127.0.0.1:6193 -H "Host: one.one.one.one" -v +// curl 127.0.0.1:6193 -H "Host: one.one.one.one" -H "Authorization: basic testcode" +// curl 127.0.0.1:6193 -H "Host: one.one.one.one" -H "Authorization: basic wrong" -v +fn main() { + env_logger::init(); + + // Create the server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service(&my_server.configuration, MyProxy); + my_proxy.add_tcp("0.0.0.0:6193"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} diff --git a/pingora_tutorial/start.md b/pingora_tutorial/start.md new file mode 100644 index 000000000..67bef6358 --- /dev/null +++ b/pingora_tutorial/start.md @@ -0,0 +1,224 @@ +# Pingora Tutorial + +The Cargo.lock and Cargo.toml are provided in the Pingora crate if you so choose to use them. Both have been labeled with the prefix of `tutorial_` so you will need to change their names before starting this tutorial. Having that will prevent you from needing to cargo add any crates that may be required to start up any of this code. If you don't want to use that then we will also make sure to point out the exact imports that we're using in each section so that you can install them yourself if you decide. + +So with that out of the way, lets begin our Pingora tutorial. + +### Introduction + +Before we begin building a full proxy server, we need a simple server to test and demonstrate our proxy’s functionality. In this tutorial, we’ll start by setting up a basic test web server in Rust that will display a simple message when accessed. This will help us verify that our proxy server is routing requests correctly. The server has already been built in it's entirety inside of the `pingora_tutorial/src/test_server.rs`. This tutorial will explain the code inside that folder so you better understand it and are able to run or modify the code as you see fit. + +Thank you for using Pingora + +## Step 1: Start the Test Web Server + +To begin, let’s create a basic server function that listens on a specified address and handles incoming client connections. This server will allow us to test the future code we’ll write for the proxy server. + +```rust +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; + +// Function that starts the test web server +pub fn start_test_server(address: &str) -> std::io::Result<()> { + let listener = TcpListener::bind(address)?; + println!("Test server running on {}", address); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(move || { + handle_test_client(stream); + }); + } + Err(e) => eprintln!("Failed to accept connection: {}", e), + } + } + Ok(()) +} +``` + +### Imports: + +```rust +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; +``` + +`std::io::{Read, Write}`: This imports Read and Write, traits that provide reading and writing functionality for streams. We’ll use these to handle data between the client and server. + +`std::net::{TcpListener, TcpStream}`: `TcpListener` allows us to listen for incoming TCP connections on a specified address. `TcpStream` represents a connection between a client and the server. + +`std::thread`: This imports Rust’s threading capabilities, allowing us to spawn new threads for each client connection to handle multiple clients concurrently. + +### Function Declaration: + +```rust +pub fn start_test_server(address: &str) -> std::io::Result<()> { +``` + +`start_test_server` is a function that takes an address parameter of type `&str` (a string slice). +It returns a `Result<(), std::io::Error>`. This indicates that the function might return an `std::io::Error` if there’s a problem, such as if the address is already in use. + +### Binding the Listener: + +```rust +let listener = TcpListener::bind(address)?; +``` + +`TcpListener::bind(address)?` creates a TCP listener that binds to the provided address (e.g., "127.0.0.1:9000"). The `?` operator automatically returns an error if binding fails. + +This listener will listen for incoming connections on the specified address. + +### Console Output: + +```rust +println!("Test server running on {}", address); +``` + +Prints a message to the console to indicate that the test server has started successfully and is running on the specified address. + +### Handling Incoming Connections: + +```rust +for stream in listener.incoming() { +``` + +`listener.incoming()` is an iterator that yields incoming connections. Each connection is represented as a `TcpStream`, allowing data transfer with a client. +We use a `for` loop to handle each incoming connection individually. + +### Match Statement and Threading: + +```rust + match stream { + Ok(stream) => { + thread::spawn(move || { + handle_test_client(stream); + }); + } + Err(e) => eprintln!("Failed to accept connection: {}", e), + } +} +``` + +`match stream`: We check whether each incoming connection (i.e., each stream) is successful (Ok) or encountered an error (Err). + +`Ok(stream)`: If a connection is established successfully, stream represents the `TcpStream` of that connection. + +`thread::spawn(move || { handle_test_client(stream); })`: Spawns a new thread for each connection, passing the `TcpStream` to `handle_test_client`. +This enables handling multiple client connections concurrently. + +`Err(e)`: If there’s an error accepting a connection, `eprintln!` outputs an error message to the console. + +### Return Value: + +```rust + Ok(()) +} +``` + +Returns `Ok(())`, indicating that the function completed successfully if there were no binding errors. + +## Step 2: Display a Response Message + +To confirm that our server is working correctly, we’ll add a function to respond with a simple HTML message, “Hello. You made it!” This will help us know when we have successfully reached the test server. + +```rust +// Function to handle each client and respond with a "Hello. You made it!" page +fn handle_test_client(mut stream: TcpStream) { + let mut buffer = [0; 512]; + if stream.read(&mut buffer).is_ok() { + // HTTP response with "Hello. You made it!" message + let response = r#"HTTP/1.1 200 OK +Content-Type: text/html + + + +Welcome + +

Hello. You made it!

+ +"#; + + // Write the response back to the client + if stream.write_all(response.as_bytes()).is_err() { + eprintln!("Failed to send response"); + } + } +} +``` + +### Code: Displaying a Response Message + +```rust +// Function to handle each client and respond with a "Hello. You made it!" page +fn handle_test_client(mut stream: TcpStream) { +``` + +### Function Declaration: + +`handle_test_client` takes a mutable `TcpStream` called stream as input, representing an open connection with a client. +This function will read the client’s request and write back a simple HTML response. + +### Buffer Initialization: + +```rust +let mut buffer = [0; 512]; +``` + +`let mut buffer = [0; 512];` creates a buffer, a fixed-size array of 512 bytes, to temporarily hold data read from the client. +512 bytes is generally enough to capture an HTTP request from the client. + +### Reading from the Client: + +```rust + if stream.read(&mut buffer).is_ok() { +``` + +`stream.read(&mut buffer).is_ok()`: Reads data from the client into buffer. The `is_ok()` check ensures that reading was successful. +We’re not inspecting the content of the request in this example; we simply need to know the client connected to respond with our message. + +### HTTP Response: + +```rust + let response = r#"HTTP/1.1 200 OK +Content-Type: text/html + + + +Welcome + +

Hello. You made it!

+ +"#; +``` + +`let response = ...`: Defines an HTTP response as a raw string (r#"...#"). +The response includes: +`Status Line: HTTP/1.1 200 OK` indicates a successful HTTP response. +`Headers: Content-Type: text/html` informs the client that the response is HTML. +`HTML Content:` Displays a simple message, `

Hello. You made it!

`, in an HTML page structure. + +This message will be displayed in the client’s browser when they successfully reach the server. + +### Writing the Response: + +```rust +if stream.write_all(response.as_bytes()).is_err() { + eprintln!("Failed to send response"); + } + } +} +``` + +`stream.write_all(response.as_bytes())`: Writes the response back to the client as bytes. + +`response.as_bytes()` converts the response string into a byte slice for sending over the network. + +`is_err()`: If an error occurs while sending the response, an error message is printed to the console with `eprintln!`. + +## Onto the next step +You have now created a rust server that will listen and respond to connections. So lets take this server and use it, as well as copies of it, to explore the rest of Pingora. + +``` Some link here getting you to the next step ``` diff --git a/pingora_tutorial/tutorial_Cargo.lock b/pingora_tutorial/tutorial_Cargo.lock new file mode 100644 index 000000000..5ed728cdc --- /dev/null +++ b/pingora_tutorial/tutorial_Cargo.lock @@ -0,0 +1,3340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64 0.22.1", + "bitflags 2.6.0", + "brotli 6.0.0", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.86", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 2.5.1", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 4.0.1", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.2", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.86", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "libz-ng-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[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 2.0.86", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-more" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libz-ng-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4436751a01da56f1277f323c80d584ffad94a3d14aecd959dd0dff75aa73a438" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.0", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.3.2+3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pingora" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9144f4950d87291365ca24e41b9a149bd38515d562a7464a6fd27ac12ca0874e" +dependencies = [ + "pingora-cache 0.3.0", + "pingora-core 0.3.0", + "pingora-http 0.3.0", + "pingora-load-balancing 0.3.0", + "pingora-proxy 0.3.0", + "pingora-timeout 0.3.0", +] + +[[package]] +name = "pingora-cache" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe55bddb694583a9db101e5ae5b31f570f2ccce312ac7d64c2e4a430510c4b3" +dependencies = [ + "ahash", + "async-trait", + "blake2", + "bytes", + "hex", + "http 1.1.0", + "httparse", + "httpdate", + "indexmap 1.9.3", + "log", + "lru", + "once_cell", + "parking_lot", + "pingora-core 0.3.0", + "pingora-error 0.3.0", + "pingora-header-serde 0.3.0", + "pingora-http 0.3.0", + "pingora-lru 0.3.0", + "pingora-timeout 0.3.0", + "regex", + "rmp", + "rmp-serde", + "rustracing", + "rustracing_jaeger", + "serde", + "tokio", +] + +[[package]] +name = "pingora-cache" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ee62f28526d8d484621e77f8d6a1807f1bd07558a06ab5a204b4834d6be056" +dependencies = [ + "ahash", + "async-trait", + "blake2", + "bytes", + "hex", + "http 1.1.0", + "httparse", + "httpdate", + "indexmap 1.9.3", + "log", + "lru", + "once_cell", + "parking_lot", + "pingora-core 0.4.0", + "pingora-error 0.4.0", + "pingora-header-serde 0.4.0", + "pingora-http 0.4.0", + "pingora-lru 0.4.0", + "pingora-timeout 0.4.0", + "regex", + "rmp", + "rmp-serde", + "rustracing", + "rustracing_jaeger", + "serde", + "strum", + "tokio", +] + +[[package]] +name = "pingora-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51dbd9509e3bb25a699bee76ba1befbffb4e733694d7e682d4bfe35a1a48cbb4" +dependencies = [ + "ahash", + "async-trait", + "brotli 3.5.0", + "bytes", + "chrono", + "clap 3.2.25", + "daemonize", + "flate2", + "futures", + "h2 0.4.6", + "http 1.1.0", + "httparse", + "httpdate", + "libc", + "log", + "lru", + "nix", + "once_cell", + "openssl-probe", + "parking_lot", + "percent-encoding", + "pingora-error 0.3.0", + "pingora-http 0.3.0", + "pingora-openssl", + "pingora-pool 0.3.0", + "pingora-runtime 0.3.0", + "pingora-timeout 0.3.0", + "prometheus", + "rand", + "regex", + "sentry", + "serde", + "serde_yaml 0.8.26", + "sfv", + "socket2", + "strum", + "strum_macros", + "thread_local", + "tokio", + "tokio-test", + "unicase", + "zstd", +] + +[[package]] +name = "pingora-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123320b69bd06e897fc16bd1dde962a7b488c4d2ae825683fbca0198fad8669" +dependencies = [ + "ahash", + "async-trait", + "brotli 3.5.0", + "bytes", + "chrono", + "clap 3.2.25", + "daemonize", + "flate2", + "futures", + "h2 0.4.6", + "http 1.1.0", + "httparse", + "httpdate", + "libc", + "log", + "lru", + "nix", + "once_cell", + "openssl-probe", + "parking_lot", + "percent-encoding", + "pingora-error 0.4.0", + "pingora-http 0.4.0", + "pingora-pool 0.4.0", + "pingora-runtime 0.4.0", + "pingora-timeout 0.4.0", + "prometheus", + "rand", + "regex", + "serde", + "serde_yaml 0.8.26", + "sfv", + "socket2", + "strum", + "strum_macros", + "thread_local", + "tokio", + "tokio-test", + "unicase", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "pingora-error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e39924926e498ddb0e64a642b6c5df56627afc0989b0f7be197eb096f998f0f" + +[[package]] +name = "pingora-error" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389511530152c535a554f592ae4a9691b1246cff20eb4564f2a34fc921195c0" + +[[package]] +name = "pingora-header-serde" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a4c91b93ffaf066443aca3ac669909695b7e93049482bb6c32125ba3366571" +dependencies = [ + "bytes", + "http 1.1.0", + "httparse", + "pingora-error 0.3.0", + "pingora-http 0.3.0", + "thread_local", + "zstd", + "zstd-safe", +] + +[[package]] +name = "pingora-header-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb3f62d852da015e76ced56e93e6d52941679a9825281c90f2897841129e59d" +dependencies = [ + "bytes", + "http 1.1.0", + "httparse", + "pingora-error 0.4.0", + "pingora-http 0.4.0", + "thread_local", + "zstd", + "zstd-safe", +] + +[[package]] +name = "pingora-http" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88243a433347f328442842d9596dfde5b8b171daaee4a3b16cbe74db033ef4cd" +dependencies = [ + "bytes", + "http 1.1.0", + "pingora-error 0.3.0", +] + +[[package]] +name = "pingora-http" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70202f126056f366549afc804741e12dd9f419cfc79a0063ab15653007a0f4c6" +dependencies = [ + "bytes", + "http 1.1.0", + "pingora-error 0.4.0", +] + +[[package]] +name = "pingora-ketama" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324f14c8f90a534ce55c11dcbc88428f60a2540265f50b5ee7010561f45dd006" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "pingora-ketama" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1bb6c2e11823a05ec9140fc8827f112b8380d78b837535f284e0a98f24cc0a" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "pingora-limits" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcc8e3afeae5a83bbcd415d8d3bb50bea31d2eda2a91f79220b59abab86dd0f" +dependencies = [ + "ahash", +] + +[[package]] +name = "pingora-load-balancing" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58b52ccfec1b55d8d903d3f78d9bb7f96115fae78065f500cb126758cc85f41" +dependencies = [ + "arc-swap", + "async-trait", + "fnv", + "futures", + "log", + "pingora-core 0.3.0", + "pingora-error 0.3.0", + "pingora-http 0.3.0", + "pingora-ketama 0.3.0", + "pingora-runtime 0.3.0", + "rand", + "tokio", +] + +[[package]] +name = "pingora-load-balancing" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d558167ecb05cea487a6479700390a67fe414724f203e10c3912584a0f2cb1" +dependencies = [ + "arc-swap", + "async-trait", + "derivative", + "fnv", + "futures", + "http 1.1.0", + "log", + "pingora-core 0.4.0", + "pingora-error 0.4.0", + "pingora-http 0.4.0", + "pingora-ketama 0.4.0", + "pingora-runtime 0.4.0", + "rand", + "tokio", +] + +[[package]] +name = "pingora-lru" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8f0df84b4b9afd63742c78e6c4b39413554f857e7d41502825e4ff9798e3e" +dependencies = [ + "arrayvec", + "hashbrown 0.15.0", + "parking_lot", + "rand", +] + +[[package]] +name = "pingora-lru" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb50f65f06c4b81ccb3edcceaa54bb9439608506b0b3b8c048798169a64aad8e" +dependencies = [ + "arrayvec", + "hashbrown 0.15.0", + "parking_lot", + "rand", +] + +[[package]] +name = "pingora-openssl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbfc0c9d7f066a6e284dfef736e3bbfee07f1cc6e27c7a6bbd2ea6a45862932" +dependencies = [ + "foreign-types", + "libc", + "openssl", + "openssl-src", + "openssl-sys", + "tokio-openssl", +] + +[[package]] +name = "pingora-pool" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d10a7b0e4115c8098e454d77a96d8ac3d89cc9a95910a6ef10f2656160d463a" +dependencies = [ + "crossbeam-queue", + "log", + "lru", + "parking_lot", + "pingora-timeout 0.3.0", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-pool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bacdd5dbdec690d468856d988b170c8bb4ab62e0edefc0f432ba5e326489f421" +dependencies = [ + "crossbeam-queue", + "log", + "lru", + "parking_lot", + "pingora-timeout 0.4.0", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-proxy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333b96aff8d7c30f04f21e3e338ba5854d66df00a7cbe437dbb35a857b84e298" +dependencies = [ + "async-trait", + "bytes", + "clap 3.2.25", + "futures", + "h2 0.4.6", + "http 1.1.0", + "log", + "once_cell", + "pingora-cache 0.3.0", + "pingora-core 0.3.0", + "pingora-error 0.3.0", + "pingora-http 0.3.0", + "pingora-timeout 0.3.0", + "regex", + "tokio", +] + +[[package]] +name = "pingora-proxy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5031783d6743bd31e4de7d7c7a19e9eecf369174c3cbd8a57eb52bc6bf882d92" +dependencies = [ + "async-trait", + "bytes", + "clap 3.2.25", + "futures", + "h2 0.4.6", + "http 1.1.0", + "log", + "once_cell", + "pingora-cache 0.4.0", + "pingora-core 0.4.0", + "pingora-error 0.4.0", + "pingora-http 0.4.0", + "pingora-timeout 0.4.0", + "regex", + "tokio", +] + +[[package]] +name = "pingora-runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f1c3302dec37b0e8916b0f637852f1e56450b7dafe6042f014343bc3cf12995" +dependencies = [ + "once_cell", + "rand", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-runtime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31a7c445ca224630961045684201e3cf8da9af0b01f286ed54ff8b2403aaabff" +dependencies = [ + "once_cell", + "rand", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-stuff" +version = "0.1.0" +dependencies = [ + "actix-web", + "async-trait", + "bytes", + "clap 4.5.20", + "cmake", + "env_logger", + "log", + "once_cell", + "pingora", + "pingora-core 0.4.0", + "pingora-error 0.4.0", + "pingora-http 0.4.0", + "pingora-limits", + "pingora-load-balancing 0.4.0", + "pingora-proxy 0.4.0", + "prometheus", + "serde", + "serde_json", + "serde_yaml 0.9.34+deprecated", +] + +[[package]] +name = "pingora-timeout" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f54daa3e32391201cfabde4dc1c2ecdfa60b4d6695ef47df56f42c55792ee3" +dependencies = [ + "futures", + "once_cell", + "parking_lot", + "pin-project-lite", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-timeout" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685bb8808cc1919c63a06ab14fdac9b84a4887ced49259a5c0adc8bfb2ffe558" +dependencies = [ + "once_cell", + "parking_lot", + "pin-project-lite", + "thread_local", + "tokio", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustracing" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44822b10c095e574869de2b891e40c724fef42cadaea040d1cd3bdbb13d36a5" +dependencies = [ + "backtrace", + "crossbeam-channel", + "rand", + "trackable 0.2.24", +] + +[[package]] +name = "rustracing_jaeger" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2fe9411ef5f43ac773f0e84ad735804c55719346a7aad52de2d9162db97c8" +dependencies = [ + "crossbeam-channel", + "hostname", + "percent-encoding", + "rand", + "rustracing", + "thrift_codec", + "trackable 0.2.24", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "sentry" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904eca4fb30c6112a1dae60c0a9e29cfb42f42129da4260f1ee20e94151b62e3" +dependencies = [ + "httpdate", + "reqwest", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-panic", + "tokio", +] + +[[package]] +name = "sentry-backtrace" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1671189d1b759879fa4bdde46c50a499abb14332ed81f84fc6f60658f41b2fdb" +dependencies = [ + "backtrace", + "lazy_static", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db80ceff16bb1a4b2689b8758e5e61e405fc4d8ff9f2d1b5b845b76ce37fa34e" +dependencies = [ + "hostname", + "libc", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c9f509d3959ed4dbbd80ca42572caad682aaa1cdd92c719e0815d0e87f82c96" +dependencies = [ + "lazy_static", + "rand", + "sentry-types", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-panic" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b442769cc34115f64393f7bfe4f863c3c38749e0c0b9613a7ae25b37c7ba53" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-types" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254b600e93e9ef00a48382c9f1e86d27884bd9a5489efa4eb9210c20c72e88a6" +dependencies = [ + "debugid", + "getrandom", + "hex", + "serde", + "serde_json", + "thiserror", + "time", + "url", + "uuid", +] + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sfv" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27daf6ed3fc7ffd5ea3ce9f684fe351c47e50f2fdbb6236e2bad0b440dbe408" +dependencies = [ + "data-encoding", + "indexmap 2.6.0", + "rust_decimal", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.86", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "thrift_codec" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb61fb3d0a0af14949f3a6949b2639112e13226647112824f4d081533f9b1a8" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.86", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/pingora_tutorial/tutorial_Cargo.toml b/pingora_tutorial/tutorial_Cargo.toml new file mode 100644 index 000000000..924b74a41 --- /dev/null +++ b/pingora_tutorial/tutorial_Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pingora-stuff" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4.9.0" +async-trait = "0.1.83" +bytes = "1.8.0" +clap = "4.5.20" +cmake = "0.1.51" +env_logger = "0.11.5" +log = "0.4.22" +once_cell = "1.20.2" +pingora = { version = "0.3.0", features = ["pingora-cache", "pingora-load-balancing", "pingora-proxy", "proxy"] } +pingora-core = "0.4.0" +pingora-error = "0.4.0" +pingora-http = "0.4.0" +pingora-limits = "0.4.0" +pingora-load-balancing = "0.4.0" +pingora-proxy = "0.4.0" +prometheus = "0.13.4" +serde = "1.0.214" +serde_json = "1.0.132" +serde_yaml = "0.9.34" diff --git a/pingora_tutorial/use_module.md b/pingora_tutorial/use_module.md new file mode 100644 index 000000000..f862d8bfb --- /dev/null +++ b/pingora_tutorial/use_module.md @@ -0,0 +1,384 @@ +# Tutorial: Adding a Custom ACL Module to a Pingora-based HTTP Proxy in Rust + +This tutorial demonstrates how to implement a **custom ACL (Access Control List)** module in a Pingora-based Rust HTTP proxy. Using Pingora’s **module system**, you can attach third-party or custom logic to inspect and modify requests before they are forwarded to an upstream. + +Below, we’ll break down each part of the code, show how the module enforces a simple **`Authorization: basic ...`** check, and explain how you can adapt it for your own needs. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Code Overview](#code-overview) + - [Imports & Dependencies](#imports--dependencies) + - [Module Basics](#module-basics) + - [Custom ACL Module (`my_acl`)](#custom-acl-module-my_acl) + 1. [Context Struct (`MyAclCtx`)](#1-context-struct-myclctx) + 2. [Module Implementation (`HttpModule`)](#2-module-implementation-httpmodule) + 3. [Module Builder (`MyAcl`)](#3-module-builder-myacl) + - [`MyProxy` Struct](#myproxy-struct) + 1. [Attaching Modules](#1-attaching-modules) + 2. [Upstream Peer Selection](#2-upstream-peer-selection) + - [Main Function & Server Setup](#main-function--server-setup) +3. [How It Works At Runtime](#how-it-works-at-runtime) +4. [Testing the Example](#testing-the-example) +5. [Extending or Modifying the Code](#extending-or-modifying-the-code) +6. [Complete Example Code](#complete-example-code) + +--- + +## Introduction + +In Pingora, **modules** are small, pluggable pieces of logic that can inspect and transform HTTP requests and responses. For example, you can create a module that: + +- **Enforces an authentication policy** +- **Adds or removes certain headers** +- **Implements rate limiting** + +In this example, our module checks whether the **`Authorization`** header matches a given credential. If it doesn’t match, the request is rejected with a `403 Forbidden` response. + +--- + +## Code Overview + +Below is an annotated version of our code. + +### Imports & Dependencies + +```rust +use async_trait::async_trait; + +use pingora_core::modules::http::HttpModules; +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::RequestHeader; +use pingora_proxy::{ProxyHttp, Session}; +use std::any::Any; +``` + +- **`async_trait`**: Allows async functions in trait implementations. +- **`pingora_core::modules::http`**: Where Pingora’s HTTP module interfaces live. +- **`Server`, `HttpPeer`, `Result`**: Core Pingora types for setting up a server, defining upstreams, and handling results. +- **`pingora_http::RequestHeader`**: Provides access to incoming HTTP request headers. +- **`pingora_proxy::{ProxyHttp, Session}`**: The main Pingora traits that let you define a proxy’s logic and interact with a request “session.” + +### Module Basics + +A **module** in Pingora is an object that implements certain hooks—like `request_header_filter`—allowing you to inspect or modify requests. You can attach these modules to a proxy at startup. Each incoming request triggers a new **module context** object. + +### Custom ACL Module (`my_acl`) + +We define our custom module logic in the `my_acl` submodule. + +#### 1. Context Struct (`MyAclCtx`) + +```rust +pub struct MyAclCtx { + credential_header: String, +} +``` + +- **`MyAclCtx`**: Holds the per-request context data for our ACL check. In this example, we store a string (`credential_header`) that the request must match in its `Authorization` header. + +#### 2. Module Implementation (`HttpModule`) + +```rust +#[async_trait] +impl HttpModule for MyAclCtx { + async fn request_header_filter(&mut self, req: &mut RequestHeader) -> Result<()> { + if let Some(auth) = req.headers.get("Authorization") { + if auth.as_bytes() != self.credential_header.as_bytes() { + return Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, credential mismatch", + ); + } else { + Ok(()) + } + } else { + Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, no auth header", + ) + } + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} +``` + +- **`HttpModule`** trait: Defines hooks like `request_header_filter`, `request_body_filter`, `response_header_filter`, etc. (We’re only using `request_header_filter` here.) +- If the `Authorization` header doesn’t match our `credential_header` string, we immediately return an error of type `ErrorType::HTTPStatus(403)`, producing a **403 Forbidden** HTTP response. +- **`as_any`** methods: Boilerplate for downcasting; required by the Pingora module framework. + +#### 3. Module Builder (`MyAcl`) + +```rust +pub struct MyAcl { + pub credential: String, +} + +impl HttpModuleBuilder for MyAcl { + fn init(&self) -> Module { + Box::new(MyAclCtx { + credential_header: format!("basic {}", self.credential), + }) + } +} +``` + +- **`MyAcl`**: The **singleton** struct that lives for the server’s lifetime. It holds data needed by the module context, such as the `credential`. +- **`HttpModuleBuilder`** trait: Lets Pingora create a fresh **module context** for each request. Here, `init` returns a new `MyAclCtx`. We format `credential_header` to `"basic "`, so we can compare it against the request’s `Authorization` header. + +--- + +### `MyProxy` Struct + +```rust +pub struct MyProxy; +``` + +A simple struct implementing `ProxyHttp`—the primary trait for defining a Pingora-based proxy. + +#### 1. Attaching Modules + +```rust +fn init_downstream_modules(&self, modules: &mut HttpModules) { + modules.add_module(Box::new(my_acl::MyAcl { + credential: "testcode".into(), + })); +} +``` + +- **`init_downstream_modules`** is called **once** when the server starts. +- We create an instance of `MyAcl` with the credential `"testcode"`. +- Then we add this module to the `HttpModules` list. When a request arrives, Pingora will call the `init()` method on each module builder, producing a context for the request. + +#### 2. Upstream Peer Selection + +```rust +async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, +) -> Result> { + let peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(peer) +} +``` + +This is where we define the **upstream**. All requests that pass our ACL check will be forwarded to **`1.1.1.1`** on port **443** with TLS enabled (SNI host: `"one.one.one.one"`). You can change this to suit your environment (e.g. `"127.0.0.1:3000"` for a local server, or a load balancer, etc.). + +--- + +## Main Function & Server Setup + +```rust +fn main() { + env_logger::init(); + + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service(&my_server.configuration, MyProxy); + my_proxy.add_tcp("0.0.0.0:6193"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +1. **`Server::new(None)`**: Creates a Pingora server with no command-line options. +2. **`bootstrap()`**: Prepares internal tasks or resources for the server. +3. **`pingora_proxy::http_proxy_service(...)`**: Builds an HTTP proxy service using `MyProxy`. +4. **`add_tcp("0.0.0.0:6193")`**: Binds the proxy to listen on port **6193**. +5. **`my_server.add_service(my_proxy)`**: Registers our proxy as a service within the Pingora server. +6. **`run_forever()`**: Starts the event loop and keeps the server running. + +--- + +## How It Works At Runtime + +1. **Server Start**: When `main()` runs, it creates the server, boots it, and attaches the `MyProxy` service. +2. **Module Registration**: `MyProxy::init_downstream_modules` is called; we register our `MyAcl` module with the server. +3. **Incoming Request**: For each request, Pingora will: + - Call `MyAcl::init()` to create a `MyAclCtx`. + - Run the module’s `request_header_filter` method. If it sees that `"Authorization: basic testcode"` is **not** present, it returns a 403. + - If the request passes the check, control proceeds to `MyProxy::upstream_peer`. + - The request is forwarded to `1.1.1.1:443`. +4. **Response**: The response from the upstream (1.1.1.1) is forwarded back to the client. + +--- + +## Testing the Example + +Compile and run this example: + +```bash +RUST_LOG=INFO cargo run +``` + +(Can also use `cargo run --example use_module` if you placed it in an `examples` folder, depending on your setup.) + +Then, test via `curl`: + +1. **No `Authorization` header** -> Expect `403 Forbidden`: + ```bash + curl -v http://127.0.0.1:6193 -H "Host: one.one.one.one" + ``` +2. **Correct `Authorization`** -> Request passes and is proxied upstream: + ```bash + curl -v http://127.0.0.1:6193 -H "Host: one.one.one.one" -H "Authorization: basic testcode" + ``` +3. **Wrong `Authorization`** -> Again, `403 Forbidden`: + ```bash + curl -v http://127.0.0.1:6193 -H "Host: one.one.one.one" -H "Authorization: basic wrong" + ``` + +--- + +## Extending or Modifying the Code + +- **Change the credential**: In `init_downstream_modules`, set your own string or generate it dynamically. +- **Use a more complex auth scheme**: Instead of checking `basic testcode`, parse JWT tokens, check a username/password store, or integrate with OAuth. +- **Multiple modules**: You can add more than one module. For instance, you could have an ACL module plus a logging or caching module. +- **Different upstream**: In `upstream_peer`, direct traffic to any HTTP or HTTPS server by changing the IP/port and the SNI host if TLS is enabled. + +--- + +## Complete Example Code + +Below is the entire code for convenience: + +```rust +use async_trait::async_trait; + +use pingora_core::modules::http::HttpModules; +use pingora_core::server::Server; +use pingora_core::upstreams::peer::HttpPeer; +use pingora_core::Result; +use pingora_http::RequestHeader; +use pingora_proxy::{ProxyHttp, Session}; +use std::any::Any; + +/// This example shows how to build and import 3rd party modules + +/// A simple ACL to check "Authorization: basic $credential" header +mod my_acl { + use super::*; + use pingora_core::modules::http::{HttpModule, HttpModuleBuilder, Module}; + use pingora_error::{Error, ErrorType}; // Import Error and ErrorType here + + // This is the struct for per-request module context + pub struct MyAclCtx { + credential_header: String, + } + + // Implement how the module would consume and/or modify request and/or response + #[async_trait] + impl HttpModule for MyAclCtx { + async fn request_header_filter(&mut self, req: &mut RequestHeader) -> Result<()> { + if let Some(auth) = req.headers.get("Authorization") { + if auth.as_bytes() != self.credential_header.as_bytes() { + return Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, credential mismatch", + ); + } else { + Ok(()) + } + } else { + Error::e_explain( + ErrorType::HTTPStatus(403), + "Auth failed, no auth header", + ) + } + } + + // Boilerplate code for all modules + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + } + + // This is the singleton object which will be attached to the server + pub struct MyAcl { + pub credential: String, + } + + impl HttpModuleBuilder for MyAcl { + // This function defines how to create each context. This function is called when a new request arrives + fn init(&self) -> Module { + Box::new(MyAclCtx { + // Make it easier to compare header + credential_header: format!("basic {}", self.credential), + }) + } + } +} + +pub struct MyProxy; + +#[async_trait] +impl ProxyHttp for MyProxy { + type CTX = (); + + fn new_ctx(&self) -> Self::CTX { + () + } + + // This function is only called once when the server starts + fn init_downstream_modules(&self, modules: &mut HttpModules) { + // Add the module to MyProxy + modules.add_module(Box::new(my_acl::MyAcl { + credential: "testcode".into(), + })); + } + + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + let peer = Box::new(HttpPeer::new( + ("1.1.1.1", 443), + true, + "one.one.one.one".to_string(), + )); + Ok(peer) + } +} + +fn main() { + env_logger::init(); + + // Create the server without command line arguments + let mut my_server = Server::new(None).unwrap(); + my_server.bootstrap(); + + let mut my_proxy = pingora_proxy::http_proxy_service(&my_server.configuration, MyProxy); + my_proxy.add_tcp("0.0.0.0:6193"); + + my_server.add_service(my_proxy); + my_server.run_forever(); +} +``` + +Use the instructions in the comments to **test** it with `curl` commands, verifying you get `403 Forbidden` when you don’t meet the `Authorization` requirement. + +--- + +**Happy coding!** Feel free to extend this example to integrate more complex authentication, logging, or transformations in your custom Pingora modules.