itsi-server 0.1.1 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of itsi-server might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +4417 -0
- data/Cargo.toml +7 -0
- data/README.md +4 -0
- data/Rakefile +8 -1
- data/_index.md +6 -0
- data/exe/itsi +94 -45
- data/ext/itsi_error/Cargo.toml +2 -0
- data/ext/itsi_error/src/from.rs +68 -0
- data/ext/itsi_error/src/lib.rs +18 -34
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +3 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
- data/ext/itsi_rb_helpers/src/lib.rs +140 -10
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -13
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +100 -40
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +141 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +282 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +388 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
- data/ext/itsi_server/src/server/bind.rs +75 -31
- data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/cache_store.rs +74 -0
- data/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/ext/itsi_server/src/server/itsi_service.rs +172 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
- data/ext/itsi_server/src/server/listener.rs +332 -132
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
- data/ext/itsi_server/src/server/mod.rs +15 -2
- data/ext/itsi_server/src/server/process_worker.rs +229 -0
- data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +337 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
- data/ext/itsi_server/src/server/signal.rs +93 -0
- data/ext/itsi_server/src/server/static_file_server.rs +984 -0
- data/ext/itsi_server/src/server/thread_worker.rs +444 -0
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/tls.rs +187 -60
- data/ext/itsi_server/src/server/types.rs +43 -0
- data/ext/itsi_tracing/Cargo.toml +5 -0
- data/ext/itsi_tracing/src/lib.rs +225 -7
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/lib/itsi/http_request.rb +87 -0
- data/lib/itsi/http_response.rb +39 -0
- data/lib/itsi/server/Itsi.rb +119 -0
- data/lib/itsi/server/config/dsl.rb +506 -0
- data/lib/itsi/server/config.rb +131 -0
- data/lib/itsi/server/default_app/default_app.rb +38 -0
- data/lib/itsi/server/default_app/index.html +91 -0
- data/lib/itsi/server/grpc_interface.rb +213 -0
- data/lib/itsi/server/rack/handler/itsi.rb +27 -0
- data/lib/itsi/server/rack_interface.rb +94 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/scheduler_mode.rb +10 -0
- data/lib/itsi/server/signal_trap.rb +29 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +90 -9
- data/lib/itsi/standard_headers.rb +86 -0
- metadata +122 -31
- data/ext/itsi_server/src/request/itsi_request.rs +0 -143
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/itsi_server.rs +0 -182
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
- data/lib/itsi/request.rb +0 -39
| @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            use super::MiddlewareLayer;
         | 
| 2 | 
            +
            use crate::{
         | 
| 3 | 
            +
                ruby_types::itsi_grpc_request::ItsiGrpcRequest,
         | 
| 4 | 
            +
                server::{
         | 
| 5 | 
            +
                    itsi_service::RequestContext,
         | 
| 6 | 
            +
                    types::{HttpRequest, HttpResponse},
         | 
| 7 | 
            +
                },
         | 
| 8 | 
            +
            };
         | 
| 9 | 
            +
            use async_trait::async_trait;
         | 
| 10 | 
            +
            use derive_more::Debug;
         | 
| 11 | 
            +
            use either::Either;
         | 
| 12 | 
            +
            use http::StatusCode;
         | 
| 13 | 
            +
            use itsi_rb_helpers::{HeapVal, HeapValue};
         | 
| 14 | 
            +
            use magnus::{block::Proc, error::Result, value::ReprValue, Symbol, Value};
         | 
| 15 | 
            +
            use std::sync::Arc;
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            #[derive(Debug)]
         | 
| 18 | 
            +
            pub struct GrpcService {
         | 
| 19 | 
            +
                service: Arc<HeapValue<Proc>>,
         | 
| 20 | 
            +
                adapter: Value, // Ruby CustomGrpcAdapter object
         | 
| 21 | 
            +
            }
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            impl GrpcService {
         | 
| 24 | 
            +
                pub fn from_value(params: HeapVal) -> magnus::error::Result<Self> {
         | 
| 25 | 
            +
                    let service = params.funcall::<_, _, Proc>(Symbol::new("[]"), ("service_proc",))?;
         | 
| 26 | 
            +
                    let adapter = params.funcall::<_, _, Value>(Symbol::new("[]"), ("adapter",))?;
         | 
| 27 | 
            +
                    Ok(GrpcService {
         | 
| 28 | 
            +
                        service: Arc::new(service.into()),
         | 
| 29 | 
            +
                        adapter,
         | 
| 30 | 
            +
                    })
         | 
| 31 | 
            +
                }
         | 
| 32 | 
            +
            }
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            #[async_trait]
         | 
| 35 | 
            +
            impl MiddlewareLayer for GrpcService {
         | 
| 36 | 
            +
                async fn before(
         | 
| 37 | 
            +
                    &self,
         | 
| 38 | 
            +
                    req: HttpRequest,
         | 
| 39 | 
            +
                    context: &mut RequestContext,
         | 
| 40 | 
            +
                ) -> Result<Either<HttpRequest, HttpResponse>> {
         | 
| 41 | 
            +
                    // Extract gRPC method and service names from the path
         | 
| 42 | 
            +
                    let path = req.uri().path();
         | 
| 43 | 
            +
                    let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
         | 
| 44 | 
            +
                    
         | 
| 45 | 
            +
                    if parts.len() < 2 {
         | 
| 46 | 
            +
                        return Ok(Either::Right(HttpResponse::new(StatusCode::BAD_REQUEST)));
         | 
| 47 | 
            +
                    }
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    let service_name = parts[0].to_string();
         | 
| 50 | 
            +
                    let method_name = parts[1].to_string();
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    // Get RPC descriptor from the adapter
         | 
| 53 | 
            +
                    let rpc_desc = self.adapter.funcall::<_, _, Option<Value>>(
         | 
| 54 | 
            +
                        "get_rpc_desc",
         | 
| 55 | 
            +
                        (service_name.clone(), method_name.clone()),
         | 
| 56 | 
            +
                    )?;
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    // Create gRPC request and process it
         | 
| 59 | 
            +
                    let (grpc_req, _) = ItsiGrpcRequest::new(
         | 
| 60 | 
            +
                        req,
         | 
| 61 | 
            +
                        context,
         | 
| 62 | 
            +
                        method_name,
         | 
| 63 | 
            +
                        service_name,
         | 
| 64 | 
            +
                        rpc_desc,
         | 
| 65 | 
            +
                    ).await;
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    grpc_req
         | 
| 68 | 
            +
                        .process(context.ruby(), self.service.clone())
         | 
| 69 | 
            +
                        .map_err(|e| e.into())
         | 
| 70 | 
            +
                        .map(|_| Either::Right(HttpResponse::new(StatusCode::OK)))
         | 
| 71 | 
            +
                }
         | 
| 72 | 
            +
            } 
         | 
| @@ -0,0 +1,85 @@ | |
| 1 | 
            +
            use http::{header::GetAll, HeaderValue};
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            /// Given a list of header values (which may be comma-separated and may have quality parameters)
         | 
| 4 | 
            +
            /// and a list of supported items (each supported item is a full value or a prefix ending with '*'),
         | 
| 5 | 
            +
            /// return Some(supported_item) for the first supported item that matches any header value, or None.
         | 
| 6 | 
            +
            pub fn find_first_supported<'a, I>(
         | 
| 7 | 
            +
                header_values: &http::header::GetAll<http::HeaderValue>,
         | 
| 8 | 
            +
                supported: I,
         | 
| 9 | 
            +
            ) -> Option<&'a str>
         | 
| 10 | 
            +
            where
         | 
| 11 | 
            +
                I: IntoIterator<Item = &'a str> + Clone,
         | 
| 12 | 
            +
            {
         | 
| 13 | 
            +
                // best candidate: (quality, supported_index, candidate)
         | 
| 14 | 
            +
                let mut best: Option<(f32, usize, &'a str)> = None;
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                for value in header_values.iter() {
         | 
| 17 | 
            +
                    if let Ok(s) = value.to_str() {
         | 
| 18 | 
            +
                        for token in s.split(',') {
         | 
| 19 | 
            +
                            let token = token.trim();
         | 
| 20 | 
            +
                            if token.is_empty() {
         | 
| 21 | 
            +
                                continue;
         | 
| 22 | 
            +
                            }
         | 
| 23 | 
            +
                            let mut parts = token.split(';');
         | 
| 24 | 
            +
                            let enc = parts.next()?.trim();
         | 
| 25 | 
            +
                            if enc.is_empty() {
         | 
| 26 | 
            +
                                continue;
         | 
| 27 | 
            +
                            }
         | 
| 28 | 
            +
                            let quality = parts
         | 
| 29 | 
            +
                                .find_map(|p| {
         | 
| 30 | 
            +
                                    let p = p.trim();
         | 
| 31 | 
            +
                                    if let Some(q_str) = p.strip_prefix("q=") {
         | 
| 32 | 
            +
                                        q_str.parse::<f32>().ok()
         | 
| 33 | 
            +
                                    } else {
         | 
| 34 | 
            +
                                        None
         | 
| 35 | 
            +
                                    }
         | 
| 36 | 
            +
                                })
         | 
| 37 | 
            +
                                .unwrap_or(1.0);
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                            // For each supported encoding, iterate over a clone of the iterable.
         | 
| 40 | 
            +
                            for (i, supp) in supported.clone().into_iter().enumerate() {
         | 
| 41 | 
            +
                                let is_match = if supp == "*" {
         | 
| 42 | 
            +
                                    true
         | 
| 43 | 
            +
                                } else if let Some(prefix) = supp.strip_suffix('*') {
         | 
| 44 | 
            +
                                    enc.starts_with(prefix)
         | 
| 45 | 
            +
                                } else {
         | 
| 46 | 
            +
                                    enc.eq_ignore_ascii_case(supp)
         | 
| 47 | 
            +
                                };
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                                if is_match {
         | 
| 50 | 
            +
                                    best = match best {
         | 
| 51 | 
            +
                                        Some((best_q, best_idx, _))
         | 
| 52 | 
            +
                                            if quality > best_q || (quality == best_q && i < best_idx) =>
         | 
| 53 | 
            +
                                        {
         | 
| 54 | 
            +
                                            Some((quality, i, supp))
         | 
| 55 | 
            +
                                        }
         | 
| 56 | 
            +
                                        None => Some((quality, i, supp)),
         | 
| 57 | 
            +
                                        _ => best,
         | 
| 58 | 
            +
                                    };
         | 
| 59 | 
            +
                                }
         | 
| 60 | 
            +
                            }
         | 
| 61 | 
            +
                        }
         | 
| 62 | 
            +
                    }
         | 
| 63 | 
            +
                }
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                best.map(|(_, _, candidate)| candidate)
         | 
| 66 | 
            +
            }
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            pub fn header_contains(header_values: &GetAll<HeaderValue>, needle: &str) -> bool {
         | 
| 69 | 
            +
                if needle == "*" {
         | 
| 70 | 
            +
                    return true;
         | 
| 71 | 
            +
                }
         | 
| 72 | 
            +
                let mut headers = header_values
         | 
| 73 | 
            +
                    .iter()
         | 
| 74 | 
            +
                    .flat_map(|value| value.to_str().unwrap_or("").split(','))
         | 
| 75 | 
            +
                    .map(|s| s.trim().split(';').next().unwrap_or(""))
         | 
| 76 | 
            +
                    .filter(|s| !s.is_empty());
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                let needle_lower = needle;
         | 
| 79 | 
            +
                if needle.ends_with('*') {
         | 
| 80 | 
            +
                    let prefix = &needle_lower[..needle_lower.len() - 1];
         | 
| 81 | 
            +
                    headers.any(|h| h.starts_with(prefix))
         | 
| 82 | 
            +
                } else {
         | 
| 83 | 
            +
                    headers.any(|h| h == needle_lower)
         | 
| 84 | 
            +
                }
         | 
| 85 | 
            +
            }
         | 
| @@ -0,0 +1,195 @@ | |
| 1 | 
            +
            use super::{ErrorResponse, FromValue, MiddlewareLayer};
         | 
| 2 | 
            +
            use crate::server::{
         | 
| 3 | 
            +
                itsi_service::RequestContext,
         | 
| 4 | 
            +
                rate_limiter::{get_ban_manager, get_rate_limiter, BanManager, RateLimiter, RateLimiterConfig},
         | 
| 5 | 
            +
                types::{HttpRequest, HttpResponse, RequestExt},
         | 
| 6 | 
            +
            };
         | 
| 7 | 
            +
            use async_trait::async_trait;
         | 
| 8 | 
            +
            use either::Either;
         | 
| 9 | 
            +
            use itsi_tracing::*;
         | 
| 10 | 
            +
            use magnus::error::Result;
         | 
| 11 | 
            +
            use regex::RegexSet;
         | 
| 12 | 
            +
            use serde::Deserialize;
         | 
| 13 | 
            +
            use std::time::Duration;
         | 
| 14 | 
            +
            use std::{
         | 
| 15 | 
            +
                collections::HashMap,
         | 
| 16 | 
            +
                sync::{Arc, OnceLock},
         | 
| 17 | 
            +
            };
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            #[derive(Debug, Clone, Deserialize)]
         | 
| 20 | 
            +
            pub struct IntrusionProtection {
         | 
| 21 | 
            +
                #[serde(skip_deserializing)]
         | 
| 22 | 
            +
                pub banned_url_pattern_matcher: OnceLock<RegexSet>,
         | 
| 23 | 
            +
                #[serde(default)]
         | 
| 24 | 
            +
                pub banned_url_patterns: Vec<String>,
         | 
| 25 | 
            +
                #[serde(skip_deserializing)]
         | 
| 26 | 
            +
                pub banned_header_pattern_matchers: OnceLock<HashMap<String, RegexSet>>,
         | 
| 27 | 
            +
                #[serde(default)]
         | 
| 28 | 
            +
                pub banned_header_patterns: HashMap<String, Vec<String>>,
         | 
| 29 | 
            +
                pub banned_time_seconds: u64,
         | 
| 30 | 
            +
                #[serde(skip_deserializing)]
         | 
| 31 | 
            +
                pub rate_limiter: OnceLock<Arc<dyn RateLimiter>>,
         | 
| 32 | 
            +
                #[serde(skip_deserializing)]
         | 
| 33 | 
            +
                pub ban_manager: OnceLock<BanManager>,
         | 
| 34 | 
            +
                pub store_config: RateLimiterConfig,
         | 
| 35 | 
            +
                pub error_response: ErrorResponse,
         | 
| 36 | 
            +
            }
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            #[async_trait]
         | 
| 39 | 
            +
            impl MiddlewareLayer for IntrusionProtection {
         | 
| 40 | 
            +
                async fn initialize(&self) -> Result<()> {
         | 
| 41 | 
            +
                    // Initialize regex matchers for URL patterns
         | 
| 42 | 
            +
                    if !self.banned_url_patterns.is_empty() {
         | 
| 43 | 
            +
                        match RegexSet::new(&self.banned_url_patterns) {
         | 
| 44 | 
            +
                            Ok(regex_set) => {
         | 
| 45 | 
            +
                                let _ = self.banned_url_pattern_matcher.set(regex_set);
         | 
| 46 | 
            +
                            }
         | 
| 47 | 
            +
                            Err(e) => {
         | 
| 48 | 
            +
                                error!("Failed to compile URL regex patterns: {:?}", e);
         | 
| 49 | 
            +
                            }
         | 
| 50 | 
            +
                        }
         | 
| 51 | 
            +
                    }
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    // Initialize regex matchers for header patterns
         | 
| 54 | 
            +
                    if !self.banned_header_patterns.is_empty() {
         | 
| 55 | 
            +
                        let mut header_matchers = HashMap::new();
         | 
| 56 | 
            +
                        for (header_name, patterns) in &self.banned_header_patterns {
         | 
| 57 | 
            +
                            if !patterns.is_empty() {
         | 
| 58 | 
            +
                                match RegexSet::new(patterns) {
         | 
| 59 | 
            +
                                    Ok(regex_set) => {
         | 
| 60 | 
            +
                                        header_matchers.insert(header_name.clone(), regex_set);
         | 
| 61 | 
            +
                                    }
         | 
| 62 | 
            +
                                    Err(e) => {
         | 
| 63 | 
            +
                                        error!(
         | 
| 64 | 
            +
                                            "Failed to compile header regex patterns for {}: {:?}",
         | 
| 65 | 
            +
                                            header_name, e
         | 
| 66 | 
            +
                                        );
         | 
| 67 | 
            +
                                    }
         | 
| 68 | 
            +
                                }
         | 
| 69 | 
            +
                            }
         | 
| 70 | 
            +
                        }
         | 
| 71 | 
            +
                        let _ = self.banned_header_pattern_matchers.set(header_matchers);
         | 
| 72 | 
            +
                    }
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    // Initialize rate limiter (used for tracking bans)
         | 
| 75 | 
            +
                    // This will automatically fall back to in-memory if Redis fails
         | 
| 76 | 
            +
                    if let Ok(limiter) = get_rate_limiter(&self.store_config).await {
         | 
| 77 | 
            +
                        let _ = self.rate_limiter.set(limiter);
         | 
| 78 | 
            +
                    }
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    // Initialize ban manager
         | 
| 81 | 
            +
                    // This will automatically fall back to in-memory if Redis fails
         | 
| 82 | 
            +
                    if let Ok(manager) = get_ban_manager(&self.store_config).await {
         | 
| 83 | 
            +
                        let _ = self.ban_manager.set(manager);
         | 
| 84 | 
            +
                    }
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                    Ok(())
         | 
| 87 | 
            +
                }
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                async fn before(
         | 
| 90 | 
            +
                    &self,
         | 
| 91 | 
            +
                    req: HttpRequest,
         | 
| 92 | 
            +
                    context: &mut RequestContext,
         | 
| 93 | 
            +
                ) -> Result<Either<HttpRequest, HttpResponse>> {
         | 
| 94 | 
            +
                    // Get client IP address from context's service
         | 
| 95 | 
            +
                    let client_ip = &context.addr;
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    // Check if the IP is already banned
         | 
| 98 | 
            +
                    if let Some(ban_manager) = self.ban_manager.get() {
         | 
| 99 | 
            +
                        match ban_manager.is_banned(client_ip).await {
         | 
| 100 | 
            +
                            Ok(Some(reason)) => {
         | 
| 101 | 
            +
                                info!("Request from banned IP {}: {}", client_ip, reason);
         | 
| 102 | 
            +
                                return Ok(Either::Right(
         | 
| 103 | 
            +
                                    self.error_response.to_http_response(&req).await,
         | 
| 104 | 
            +
                                ));
         | 
| 105 | 
            +
                            }
         | 
| 106 | 
            +
                            Err(e) => {
         | 
| 107 | 
            +
                                error!("Error checking IP ban status: {:?}", e);
         | 
| 108 | 
            +
                                // Continue processing - fail open
         | 
| 109 | 
            +
                            }
         | 
| 110 | 
            +
                            _ => {
         | 
| 111 | 
            +
                                // Not banned, continue with intrusion checks
         | 
| 112 | 
            +
                            }
         | 
| 113 | 
            +
                        }
         | 
| 114 | 
            +
                    } else {
         | 
| 115 | 
            +
                        warn!("No ban manager available for intrusion protection");
         | 
| 116 | 
            +
                    }
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    // Check for banned URL patterns
         | 
| 119 | 
            +
                    if let Some(url_matcher) = self.banned_url_pattern_matcher.get() {
         | 
| 120 | 
            +
                        let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("");
         | 
| 121 | 
            +
                        info!("Checking URL pattern match for {}", path);
         | 
| 122 | 
            +
                        if url_matcher.is_match(path) {
         | 
| 123 | 
            +
                            info!("Intrusion detected: URL pattern match for {}", path);
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                            // Ban the IP address if possible
         | 
| 126 | 
            +
                            if let Some(ban_manager) = self.ban_manager.get() {
         | 
| 127 | 
            +
                                match ban_manager
         | 
| 128 | 
            +
                                    .ban_ip(
         | 
| 129 | 
            +
                                        client_ip,
         | 
| 130 | 
            +
                                        &format!("Banned URL pattern detected: {}", path),
         | 
| 131 | 
            +
                                        Duration::from_secs(self.banned_time_seconds),
         | 
| 132 | 
            +
                                    )
         | 
| 133 | 
            +
                                    .await
         | 
| 134 | 
            +
                                {
         | 
| 135 | 
            +
                                    Ok(_) => info!(
         | 
| 136 | 
            +
                                        "Successfully banned IP {} for {} seconds",
         | 
| 137 | 
            +
                                        client_ip, self.banned_time_seconds
         | 
| 138 | 
            +
                                    ),
         | 
| 139 | 
            +
                                    Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
         | 
| 140 | 
            +
                                }
         | 
| 141 | 
            +
                            }
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                            // Always return the error response even if banning failed
         | 
| 144 | 
            +
                            return Ok(Either::Right(
         | 
| 145 | 
            +
                                self.error_response.to_http_response(&req).await,
         | 
| 146 | 
            +
                            ));
         | 
| 147 | 
            +
                        }
         | 
| 148 | 
            +
                    }
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                    // Check for banned header patterns
         | 
| 151 | 
            +
                    if let Some(header_matchers) = self.banned_header_pattern_matchers.get() {
         | 
| 152 | 
            +
                        for (header_name, pattern_set) in header_matchers {
         | 
| 153 | 
            +
                            if let Some(header_value) = req.header(header_name) {
         | 
| 154 | 
            +
                                if pattern_set.is_match(header_value) {
         | 
| 155 | 
            +
                                    info!(
         | 
| 156 | 
            +
                                        "Intrusion detected: Header pattern match for {} in header {}",
         | 
| 157 | 
            +
                                        header_value, header_name
         | 
| 158 | 
            +
                                    );
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                                    // Ban the IP address if possible
         | 
| 161 | 
            +
                                    if let Some(ban_manager) = self.ban_manager.get() {
         | 
| 162 | 
            +
                                        match ban_manager
         | 
| 163 | 
            +
                                            .ban_ip(
         | 
| 164 | 
            +
                                                client_ip,
         | 
| 165 | 
            +
                                                &format!(
         | 
| 166 | 
            +
                                                    "Banned header pattern detected: {} in {}",
         | 
| 167 | 
            +
                                                    header_value, header_name
         | 
| 168 | 
            +
                                                ),
         | 
| 169 | 
            +
                                                Duration::from_secs(self.banned_time_seconds),
         | 
| 170 | 
            +
                                            )
         | 
| 171 | 
            +
                                            .await
         | 
| 172 | 
            +
                                        {
         | 
| 173 | 
            +
                                            Ok(_) => info!(
         | 
| 174 | 
            +
                                                "Successfully banned IP {} for {} seconds",
         | 
| 175 | 
            +
                                                client_ip, self.banned_time_seconds
         | 
| 176 | 
            +
                                            ),
         | 
| 177 | 
            +
                                            Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
         | 
| 178 | 
            +
                                        }
         | 
| 179 | 
            +
                                    }
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                                    // Always return the error response even if banning failed
         | 
| 182 | 
            +
                                    return Ok(Either::Right(
         | 
| 183 | 
            +
                                        self.error_response.to_http_response(&req).await,
         | 
| 184 | 
            +
                                    ));
         | 
| 185 | 
            +
                                }
         | 
| 186 | 
            +
                            }
         | 
| 187 | 
            +
                        }
         | 
| 188 | 
            +
                    }
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                    // No intrusion detected
         | 
| 191 | 
            +
                    Ok(Either::Left(req))
         | 
| 192 | 
            +
                }
         | 
| 193 | 
            +
            }
         | 
| 194 | 
            +
             | 
| 195 | 
            +
            impl FromValue for IntrusionProtection {}
         | 
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            use async_trait::async_trait;
         | 
| 2 | 
            +
            use either::Either;
         | 
| 3 | 
            +
            use itsi_tracing::*;
         | 
| 4 | 
            +
            use magnus::error::Result;
         | 
| 5 | 
            +
            use serde::Deserialize;
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            use crate::server::itsi_service::RequestContext;
         | 
| 8 | 
            +
            use crate::server::types::{HttpRequest, HttpResponse};
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            use super::string_rewrite::StringRewrite;
         | 
| 11 | 
            +
            use super::{FromValue, MiddlewareLayer};
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            /// Logging middleware for HTTP requests and responses
         | 
| 14 | 
            +
            ///
         | 
| 15 | 
            +
            /// Supports customizable log formats with placeholders
         | 
| 16 | 
            +
            #[derive(Debug, Clone, Deserialize)]
         | 
| 17 | 
            +
            pub struct LogRequests {
         | 
| 18 | 
            +
                pub before: Option<LogConfig>,
         | 
| 19 | 
            +
                pub after: Option<LogConfig>,
         | 
| 20 | 
            +
            }
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            #[derive(Debug, Clone, Deserialize)]
         | 
| 23 | 
            +
            pub struct LogConfig {
         | 
| 24 | 
            +
                level: LogMiddlewareLevel,
         | 
| 25 | 
            +
                format: StringRewrite,
         | 
| 26 | 
            +
            }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            #[derive(Debug, Clone, Deserialize)]
         | 
| 29 | 
            +
            pub enum LogMiddlewareLevel {
         | 
| 30 | 
            +
                #[serde(rename(deserialize = "INFO"))]
         | 
| 31 | 
            +
                Info,
         | 
| 32 | 
            +
                #[serde(rename(deserialize = "TRACE"))]
         | 
| 33 | 
            +
                Trace,
         | 
| 34 | 
            +
                #[serde(rename(deserialize = "DEBUG"))]
         | 
| 35 | 
            +
                Debug,
         | 
| 36 | 
            +
                #[serde(rename(deserialize = "WARN"))]
         | 
| 37 | 
            +
                Warn,
         | 
| 38 | 
            +
                #[serde(rename(deserialize = "ERROR"))]
         | 
| 39 | 
            +
                Error,
         | 
| 40 | 
            +
            }
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            impl LogMiddlewareLevel {
         | 
| 43 | 
            +
                pub fn log(&self, message: String) {
         | 
| 44 | 
            +
                    match self {
         | 
| 45 | 
            +
                        LogMiddlewareLevel::Trace => trace!(message),
         | 
| 46 | 
            +
                        LogMiddlewareLevel::Debug => debug!(message),
         | 
| 47 | 
            +
                        LogMiddlewareLevel::Info => info!(message),
         | 
| 48 | 
            +
                        LogMiddlewareLevel::Warn => warn!(message),
         | 
| 49 | 
            +
                        LogMiddlewareLevel::Error => error!(message),
         | 
| 50 | 
            +
                    }
         | 
| 51 | 
            +
                }
         | 
| 52 | 
            +
            }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            #[async_trait]
         | 
| 55 | 
            +
            impl MiddlewareLayer for LogRequests {
         | 
| 56 | 
            +
                async fn initialize(&self) -> Result<()> {
         | 
| 57 | 
            +
                    Ok(())
         | 
| 58 | 
            +
                }
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                async fn before(
         | 
| 61 | 
            +
                    &self,
         | 
| 62 | 
            +
                    req: HttpRequest,
         | 
| 63 | 
            +
                    context: &mut RequestContext,
         | 
| 64 | 
            +
                ) -> Result<Either<HttpRequest, HttpResponse>> {
         | 
| 65 | 
            +
                    context.track_start_time();
         | 
| 66 | 
            +
                    if let Some(LogConfig { level, format }) = self.before.as_ref() {
         | 
| 67 | 
            +
                        level.log(format.rewrite_request(&req, context));
         | 
| 68 | 
            +
                    }
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    Ok(Either::Left(req))
         | 
| 71 | 
            +
                }
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
         | 
| 74 | 
            +
                    if let Some(LogConfig { level, format }) = self.after.as_ref() {
         | 
| 75 | 
            +
                        level.log(format.rewrite_response(&resp, context));
         | 
| 76 | 
            +
                    }
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    resp
         | 
| 79 | 
            +
                }
         | 
| 80 | 
            +
            }
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            impl FromValue for LogRequests {}
         | 
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            mod allow_list;
         | 
| 2 | 
            +
            mod auth_api_key;
         | 
| 3 | 
            +
            mod auth_basic;
         | 
| 4 | 
            +
            mod auth_jwt;
         | 
| 5 | 
            +
            mod cache_control;
         | 
| 6 | 
            +
            mod compression;
         | 
| 7 | 
            +
            mod cors;
         | 
| 8 | 
            +
            mod deny_list;
         | 
| 9 | 
            +
            mod error_response;
         | 
| 10 | 
            +
            mod etag;
         | 
| 11 | 
            +
            mod header_interpretation;
         | 
| 12 | 
            +
            mod intrusion_protection;
         | 
| 13 | 
            +
            mod log_requests;
         | 
| 14 | 
            +
            mod proxy;
         | 
| 15 | 
            +
            mod rate_limit;
         | 
| 16 | 
            +
            mod redirect;
         | 
| 17 | 
            +
            mod request_headers;
         | 
| 18 | 
            +
            mod response_headers;
         | 
| 19 | 
            +
            mod ruby_app;
         | 
| 20 | 
            +
            mod static_assets;
         | 
| 21 | 
            +
            mod string_rewrite;
         | 
| 22 | 
            +
            mod token_source;
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            pub use allow_list::AllowList;
         | 
| 25 | 
            +
            use async_trait::async_trait;
         | 
| 26 | 
            +
            pub use auth_api_key::AuthAPIKey;
         | 
| 27 | 
            +
            pub use auth_basic::AuthBasic;
         | 
| 28 | 
            +
            pub use auth_jwt::AuthJwt;
         | 
| 29 | 
            +
            pub use cache_control::CacheControl;
         | 
| 30 | 
            +
            pub use compression::Compression;
         | 
| 31 | 
            +
            pub use compression::CompressionAlgorithm;
         | 
| 32 | 
            +
            pub use cors::Cors;
         | 
| 33 | 
            +
            pub use deny_list::DenyList;
         | 
| 34 | 
            +
            use either::Either;
         | 
| 35 | 
            +
            pub use error_response::ErrorResponse;
         | 
| 36 | 
            +
            pub use etag::ETag;
         | 
| 37 | 
            +
            pub use intrusion_protection::IntrusionProtection;
         | 
| 38 | 
            +
            pub use log_requests::LogRequests;
         | 
| 39 | 
            +
            use magnus::error::Result;
         | 
| 40 | 
            +
            use magnus::Value;
         | 
| 41 | 
            +
            pub use proxy::Proxy;
         | 
| 42 | 
            +
            pub use rate_limit::RateLimit;
         | 
| 43 | 
            +
            pub use redirect::Redirect;
         | 
| 44 | 
            +
            pub use request_headers::RequestHeaders;
         | 
| 45 | 
            +
            pub use response_headers::ResponseHeaders;
         | 
| 46 | 
            +
            pub use ruby_app::RubyApp;
         | 
| 47 | 
            +
            use serde::Deserialize;
         | 
| 48 | 
            +
            use serde_magnus::deserialize;
         | 
| 49 | 
            +
            pub use static_assets::StaticAssets;
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            use crate::server::itsi_service::RequestContext;
         | 
| 52 | 
            +
            use crate::server::types::{HttpRequest, HttpResponse};
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            pub trait FromValue: Sized + Send + Sync + 'static {
         | 
| 55 | 
            +
                fn from_value(value: Value) -> Result<Self>
         | 
| 56 | 
            +
                where
         | 
| 57 | 
            +
                    Self: Deserialize<'static>,
         | 
| 58 | 
            +
                {
         | 
| 59 | 
            +
                    deserialize(value)
         | 
| 60 | 
            +
                }
         | 
| 61 | 
            +
            }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            #[async_trait]
         | 
| 64 | 
            +
            pub trait MiddlewareLayer: Sized + Send + Sync + 'static {
         | 
| 65 | 
            +
                /// Called just once, to initialize the middleware state.
         | 
| 66 | 
            +
                async fn initialize(&self) -> Result<()> {
         | 
| 67 | 
            +
                    Ok(())
         | 
| 68 | 
            +
                }
         | 
| 69 | 
            +
                /// The "before" hook. By default, it passes through the request.
         | 
| 70 | 
            +
                async fn before(
         | 
| 71 | 
            +
                    &self,
         | 
| 72 | 
            +
                    req: HttpRequest,
         | 
| 73 | 
            +
                    _context: &mut RequestContext,
         | 
| 74 | 
            +
                ) -> Result<Either<HttpRequest, HttpResponse>> {
         | 
| 75 | 
            +
                    Ok(Either::Left(req))
         | 
| 76 | 
            +
                }
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                /// The "after" hook. By default, it passes through the response.
         | 
| 79 | 
            +
                async fn after(&self, resp: HttpResponse, _context: &mut RequestContext) -> HttpResponse {
         | 
| 80 | 
            +
                    resp
         | 
| 81 | 
            +
                }
         | 
| 82 | 
            +
            }
         |