├── .gitignore
├── src
├── proxy
│ ├── mod.rs
│ ├── error.rs
│ ├── middleware.rs
│ └── service.rs
├── middlewares
│ ├── mod.rs
│ ├── health.rs
│ ├── logger.rs
│ ├── cors.rs
│ └── router.rs
└── lib.rs
├── .github
└── workflows
│ └── rust.yml
├── Cargo.toml
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 |
--------------------------------------------------------------------------------
/src/proxy/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod error;
2 | pub mod middleware;
3 | pub mod service;
4 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Build
13 | run: cargo build --verbose
14 | - name: Run tests
15 | run: cargo test --verbose
16 |
--------------------------------------------------------------------------------
/src/middlewares/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "cors")]
2 | pub mod cors;
3 | #[cfg(feature = "health")]
4 | pub mod health;
5 | pub mod logger;
6 | #[cfg(feature = "router")]
7 | pub mod router;
8 |
9 | #[cfg(feature = "cors")]
10 | pub use self::cors::Cors;
11 | #[cfg(feature = "health")]
12 | pub use self::health::Health;
13 | pub use self::logger::Logger;
14 | #[cfg(feature = "router")]
15 | pub use self::router::Router;
16 |
--------------------------------------------------------------------------------
/src/middlewares/health.rs:
--------------------------------------------------------------------------------
1 | use hyper::{Body, Request, Response};
2 |
3 | use crate::proxy::error::MiddlewareError;
4 | use crate::proxy::middleware::MiddlewareResult::{Next, RespondWith};
5 | use crate::proxy::middleware::{Middleware, MiddlewareResult};
6 | use crate::proxy::service::{ServiceContext, State};
7 |
8 | pub struct Health {
9 | route: &'static str,
10 | raw_body: &'static str,
11 | }
12 |
13 | impl Health {
14 | pub fn new(route: &'static str, raw_body: &'static str) -> Self {
15 | Health { route, raw_body }
16 | }
17 | }
18 |
19 | impl Middleware for Health {
20 | fn name() -> String {
21 | String::from("Health")
22 | }
23 |
24 | fn before_request(
25 | &mut self,
26 | req: &mut Request
,
27 | _context: &ServiceContext,
28 | _state: &State,
29 | ) -> Result {
30 | if req.uri().path() == self.route {
31 | let ok: Response = Response::new(Body::from(self.raw_body));
32 | return Ok(RespondWith(ok));
33 | }
34 | Ok(Next)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "simple_proxy"
3 | edition = "2018"
4 | version = "1.3.4"
5 | authors = ["Terry Raimondo "]
6 | description = "Simple proxy with middlewares, easy to customize, easy to use."
7 | license = "Apache-2.0"
8 | homepage = "https://github.com/terry90/rs-simple-proxy"
9 | repository = "https://github.com/terry90/rs-simple-proxy"
10 |
11 | [package.metadata.docs.rs]
12 | features = ["docs"]
13 |
14 | [features]
15 | router = ["regex", "serde_regex"]
16 | health = []
17 | cors = []
18 | docs = ["router", "health", "cors"]
19 |
20 | [dependencies]
21 | futures = "0.3.5"
22 | log = "0.4.8"
23 | chrono = { version = "0.4.11", features = ["serde"] }
24 | regex = { version = "1.3.9", optional = true }
25 | serde_regex = { version = "1.1.0", optional = true }
26 | serde_json = "1.0.55"
27 | serde_derive = "1.0.112"
28 | serde = "1.0.112"
29 | rand = { version = "0.8.3", features = ["small_rng"] }
30 | hyper = { version = "0.14.5", features = ["client", "tcp", "http1", "server"] }
31 | http = "0.2.1"
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple proxy
2 |
3 | ## Usage
4 |
5 | ```rust
6 | use simple_proxy::middlewares::{router::*, Logger};
7 | use simple_proxy::{Environment, SimpleProxy};
8 |
9 | use structopt::StructOpt;
10 |
11 | #[derive(StructOpt, Debug)]
12 | struct Cli {
13 | port: u16,
14 | }
15 |
16 | #[derive(Debug, Clone)]
17 | pub struct Config();
18 |
19 | impl RouterConfig for Config {
20 | fn get_router_filename(&self) -> &'static str {
21 | "routes.json"
22 | }
23 | }
24 |
25 | #[tokio::main]
26 | async fn main() {
27 | let args = Cli::from_args();
28 |
29 | let mut proxy = SimpleProxy::new(args.port, Environment::Development);
30 | let logger = Logger::new();
31 | let router = Router::new(&Config());
32 |
33 | // Order matters
34 | proxy.add_middleware(Box::new(router));
35 | proxy.add_middleware(Box::new(logger));
36 |
37 | // Start proxy
38 | let _ = proxy.run().await;
39 | }
40 | ```
41 |
42 | ### Custom middleware
43 |
44 | You can create your custom middleware by creating a struct implementing Middleware, consisting of 4 callbacks:
45 |
46 | - `before_request` will be run every time
47 | - `request_failure` will be run when the request fails
48 | - `request_success` will be run when the request succeeds, you can then handle the response according to the status code or the body
49 | - `after_request` will be run every time
50 |
51 | #### For more info, see a [default middleware](src/middlewares/logger.rs)
52 |
--------------------------------------------------------------------------------
/src/proxy/error.rs:
--------------------------------------------------------------------------------
1 | use hyper::{Body, Response, StatusCode};
2 | use std::error::Error;
3 |
4 | #[derive(Debug)]
5 | pub struct MiddlewareError {
6 | pub description: String,
7 | pub body: String,
8 | pub status: StatusCode,
9 | }
10 |
11 | impl From for Response {
12 | fn from(err: MiddlewareError) -> Response {
13 | err.to_json_response()
14 | }
15 | }
16 |
17 | impl MiddlewareError {
18 | pub fn new(description: String, body: Option, status: StatusCode) -> MiddlewareError {
19 | let body = match body {
20 | Some(body) => body,
21 | None => {
22 | // Uncatched error
23 | let err = format!("Internal proxy server error: {}", &description);
24 | error!("{}", &err);
25 | err
26 | }
27 | };
28 |
29 | debug!("Middleware error: {}", &description);
30 |
31 | MiddlewareError {
32 | description,
33 | status,
34 | body,
35 | }
36 | }
37 |
38 | pub fn to_json_response(&self) -> Response {
39 | Response::builder()
40 | .header("Content-Type", "application/json")
41 | .status(self.status)
42 | .body(Body::from(format!("{{\"error\":\"{}\"}}", self.body)))
43 | .unwrap()
44 | }
45 | }
46 |
47 | impl From for MiddlewareError
48 | where
49 | E: Error,
50 | {
51 | fn from(err: E) -> MiddlewareError {
52 | MiddlewareError::new(err.to_string(), None, StatusCode::INTERNAL_SERVER_ERROR)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/middlewares/logger.rs:
--------------------------------------------------------------------------------
1 | use chrono::{DateTime, Utc};
2 | use hyper::{Body, Request, Response};
3 | use serde_json;
4 |
5 | use crate::proxy::error::MiddlewareError;
6 | use crate::proxy::middleware::MiddlewareResult::Next;
7 | use crate::proxy::middleware::{Middleware, MiddlewareResult};
8 | use crate::proxy::service::{ServiceContext, State};
9 |
10 | #[derive(Clone, Default)]
11 | pub struct Logger;
12 |
13 | /// # Panics
14 | /// May panic if the request state has not been initialized in `before_request`.
15 | /// e.g If a middleware responded early before the logger in `before_request`.
16 | impl Middleware for Logger {
17 | fn name() -> String {
18 | String::from("Logger")
19 | }
20 |
21 | fn before_request(
22 | &mut self,
23 | req: &mut Request,
24 | context: &ServiceContext,
25 | state: &State,
26 | ) -> Result {
27 | info!(
28 | "[{}] Starting a {} request to {}",
29 | &context.req_id.to_string()[..6],
30 | req.method(),
31 | req.uri()
32 | );
33 | let now = serde_json::to_string(&Utc::now()).expect("[Logger] Cannot serialize DateTime");
34 | self.set_state(context.req_id, state, now)?;
35 | Ok(Next)
36 | }
37 |
38 | fn after_request(
39 | &mut self,
40 | _res: Option<&mut Response>,
41 | context: &ServiceContext,
42 | state: &State,
43 | ) -> Result {
44 | let start_time = self.get_state(context.req_id, state)?;
45 | match start_time {
46 | Some(time) => {
47 | let start_time: DateTime = serde_json::from_str(&time)?;
48 |
49 | info!(
50 | "[{}] Request took {}ms",
51 | &context.req_id.to_string()[..6],
52 | (Utc::now() - start_time).num_milliseconds()
53 | );
54 | }
55 | None => error!("[Logger] start time not found in state"),
56 | }
57 | Ok(Next)
58 | }
59 | }
60 |
61 | impl Logger {
62 | pub fn new() -> Self {
63 | Logger {}
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/middlewares/cors.rs:
--------------------------------------------------------------------------------
1 | use hyper::header::HeaderValue;
2 | use hyper::{Body, Method, Request, Response};
3 |
4 | use crate::proxy::error::MiddlewareError;
5 | use crate::proxy::middleware::MiddlewareResult::Next;
6 | use crate::proxy::middleware::MiddlewareResult::RespondWith;
7 | use crate::proxy::middleware::{Middleware, MiddlewareResult};
8 | use crate::proxy::service::{ServiceContext, State};
9 |
10 | pub struct Cors {
11 | allow_origin: &'static str,
12 | allow_methods: &'static str,
13 | allow_headers: &'static str,
14 | }
15 |
16 | impl Cors {
17 | pub fn new(
18 | allow_origin: &'static str,
19 | allow_methods: &'static str,
20 | allow_headers: &'static str,
21 | ) -> Self {
22 | Cors {
23 | allow_origin,
24 | allow_methods,
25 | allow_headers,
26 | }
27 | }
28 |
29 | fn set_cors_headers(&self, response: &mut Response) {
30 | response.headers_mut().insert(
31 | "Access-Control-Allow-Origin",
32 | HeaderValue::from_static(self.allow_origin),
33 | );
34 | response.headers_mut().insert(
35 | "Access-Control-Allow-Methods",
36 | HeaderValue::from_static(self.allow_methods),
37 | );
38 | response.headers_mut().insert(
39 | "Access-Control-Allow-Headers",
40 | HeaderValue::from_static(self.allow_headers),
41 | );
42 | }
43 | }
44 |
45 | impl Middleware for Cors {
46 | fn name() -> String {
47 | String::from("Cors")
48 | }
49 |
50 | fn before_request(
51 | &mut self,
52 | req: &mut Request,
53 | _context: &ServiceContext,
54 | _state: &State,
55 | ) -> Result {
56 | if req.method() == Method::OPTIONS {
57 | let mut response: Response = Response::new(Body::from(""));
58 | self.set_cors_headers(&mut response);
59 |
60 | return Ok(RespondWith(response));
61 | }
62 | Ok(Next)
63 | }
64 |
65 | fn after_request(
66 | &mut self,
67 | response: Option<&mut Response>,
68 | _context: &ServiceContext,
69 | _state: &State,
70 | ) -> Result {
71 | if let Some(res) = response {
72 | self.set_cors_headers(res);
73 | }
74 | Ok(Next)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/proxy/middleware.rs:
--------------------------------------------------------------------------------
1 | use crate::proxy::error::MiddlewareError;
2 | use crate::proxy::service::{ServiceContext, State};
3 | use hyper::{Body, Error, Request, Response};
4 |
5 | pub enum MiddlewareResult {
6 | RespondWith(Response),
7 | Next,
8 | }
9 |
10 | use self::MiddlewareResult::Next;
11 |
12 | pub trait Middleware {
13 | fn name() -> String
14 | where
15 | Self: Sized;
16 |
17 | fn get_name(&self) -> String
18 | where
19 | Self: Sized,
20 | {
21 | Self::name()
22 | }
23 |
24 | fn set_state(&self, req_id: u64, state: &State, data: String) -> Result<(), MiddlewareError>
25 | where
26 | Self: Sized,
27 | {
28 | let mut state = state.lock()?;
29 | state.insert((self.get_name(), req_id), data);
30 | Ok(())
31 | }
32 |
33 | fn state(req_id: u64, state: &State) -> Result