itsi 0.1.11 → 0.1.12
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.
- checksums.yaml +4 -4
- data/Cargo.lock +1535 -45
- data/{sandbox/itsi_itsi_file/Itsi.rb → Itsi.rb} +19 -13
- data/Rakefile +8 -7
- data/crates/itsi_error/src/lib.rs +9 -0
- data/crates/itsi_rb_helpers/Cargo.toml +1 -0
- data/crates/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/crates/itsi_rb_helpers/src/lib.rs +34 -7
- data/crates/itsi_server/Cargo.toml +69 -30
- data/crates/itsi_server/src/lib.rs +79 -147
- data/crates/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/crates/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
- data/crates/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
- data/{gems/server/ext/itsi_server/src/request/itsi_request.rs → crates/itsi_server/src/ruby_types/itsi_http_request.rs} +101 -117
- data/crates/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +82 -0
- data/crates/itsi_server/src/ruby_types/mod.rs +55 -0
- data/crates/itsi_server/src/server/bind.rs +13 -5
- data/crates/itsi_server/src/server/byte_frame.rs +32 -0
- data/crates/itsi_server/src/server/cache_store.rs +74 -0
- data/crates/itsi_server/src/server/itsi_service.rs +172 -0
- data/crates/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/crates/itsi_server/src/server/listener.rs +102 -2
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +315 -0
- data/crates/itsi_server/src/server/mod.rs +8 -1
- data/crates/itsi_server/src/server/process_worker.rs +38 -12
- data/crates/itsi_server/src/server/rate_limiter.rs +565 -0
- data/crates/itsi_server/src/server/request_job.rs +11 -0
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
- data/crates/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
- data/crates/itsi_server/src/server/signal.rs +19 -0
- data/crates/itsi_server/src/server/static_file_server.rs +984 -0
- data/crates/itsi_server/src/server/thread_worker.rs +139 -94
- data/crates/itsi_server/src/server/types.rs +43 -0
- data/crates/itsi_server/test.md +14 -0
- data/crates/itsi_tracing/Cargo.toml +1 -0
- data/crates/itsi_tracing/src/lib.rs +216 -45
- data/docs/.gitignore +7 -0
- data/docs/.gitpod.yml +15 -0
- data/docs/Itsi.rb +17 -0
- data/docs/content/_index.md +17 -0
- data/docs/content/about.md +6 -0
- data/docs/content/docs/_index.md +18 -0
- data/docs/content/docs/first-page.md +9 -0
- data/docs/content/docs/folder/_index.md +10 -0
- data/docs/content/docs/folder/leaf.md +7 -0
- data/docs/go.mod +5 -0
- data/docs/go.sum +2 -0
- data/docs/hugo.yaml +77 -0
- data/examples/static_assets_example.rb +83 -0
- data/gems/_index.md +18 -0
- data/gems/scheduler/CODE_OF_CONDUCT.md +7 -0
- data/gems/scheduler/Cargo.lock +75 -14
- data/gems/scheduler/README.md +5 -0
- data/gems/scheduler/_index.md +7 -0
- data/gems/scheduler/itsi-scheduler.gemspec +4 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/lib/itsi/scheduler.rb +2 -2
- data/gems/scheduler/test/test_file_io.rb +0 -1
- data/gems/scheduler/test/test_itsi_scheduler.rb +1 -1
- data/gems/server/CHANGELOG.md +5 -0
- data/gems/server/CODE_OF_CONDUCT.md +7 -0
- data/gems/server/Cargo.lock +1536 -45
- data/gems/server/README.md +4 -0
- data/gems/server/_index.md +6 -0
- data/gems/server/exe/itsi +33 -74
- data/gems/server/itsi-server.gemspec +3 -2
- data/gems/server/lib/itsi/{request.rb → http_request.rb} +29 -5
- data/gems/server/lib/itsi/http_response.rb +39 -0
- data/gems/server/lib/itsi/server/Itsi.rb +11 -19
- data/gems/server/lib/itsi/server/config/dsl.rb +506 -0
- data/gems/server/lib/itsi/server/config.rb +103 -8
- data/gems/server/lib/itsi/server/default_app/default_app.rb +38 -0
- data/gems/server/lib/itsi/server/grpc_interface.rb +213 -0
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +8 -17
- data/gems/server/lib/itsi/server/rack_interface.rb +23 -4
- data/gems/server/lib/itsi/server/scheduler_interface.rb +1 -1
- data/gems/server/lib/itsi/server/scheduler_mode.rb +4 -0
- data/gems/server/lib/itsi/server/signal_trap.rb +7 -1
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +74 -63
- data/gems/server/lib/itsi/standard_headers.rb +86 -0
- data/gems/server/test/helpers/test_helper.rb +12 -12
- data/gems/server/test/test_itsi_server.rb +2 -2
- data/lib/itsi/version.rb +1 -1
- data/sandbox/itsi_file/Gemfile +11 -0
- data/sandbox/itsi_file/Gemfile.lock +69 -0
- data/sandbox/itsi_file/Itsi.rb +276 -0
- data/sandbox/itsi_file/error.html +2 -0
- data/sandbox/itsi_file/organisations_controller.rb +20 -0
- data/sandbox/itsi_file/public/assets/image.png +0 -0
- data/sandbox/itsi_file/public/assets/index.html +1 -0
- data/sandbox/itsi_sandbox_hanami/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rack/config.ru +2 -15
- data/sandbox/itsi_sandbox_rails/.dockerignore +2 -5
- data/sandbox/itsi_sandbox_rails/.github/workflows/ci.yml +1 -1
- data/sandbox/itsi_sandbox_rails/.gitignore +2 -1
- data/sandbox/itsi_sandbox_rails/Dockerfile +6 -9
- data/sandbox/itsi_sandbox_rails/Gemfile +16 -22
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +100 -225
- data/sandbox/itsi_sandbox_rails/app/assets/config/manifest.js +4 -0
- data/sandbox/itsi_sandbox_rails/app/assets/stylesheets/application.css +11 -6
- data/sandbox/itsi_sandbox_rails/app/channels/application_cable/channel.rb +4 -0
- data/sandbox/itsi_sandbox_rails/app/channels/application_cable/connection.rb +4 -0
- data/sandbox/itsi_sandbox_rails/app/controllers/live_controller.rb +7 -8
- data/sandbox/itsi_sandbox_rails/app/controllers/uploads_controller.rb +0 -3
- data/sandbox/itsi_sandbox_rails/app/views/layouts/application.html.erb +2 -7
- data/sandbox/itsi_sandbox_rails/bin/docker-entrypoint +3 -4
- data/sandbox/itsi_sandbox_rails/bin/setup +8 -5
- data/sandbox/itsi_sandbox_rails/config/application.rb +1 -35
- data/sandbox/itsi_sandbox_rails/config/cable.yml +3 -10
- data/sandbox/itsi_sandbox_rails/config/credentials.yml.enc +1 -1
- data/sandbox/itsi_sandbox_rails/config/database.yml +9 -19
- data/sandbox/itsi_sandbox_rails/config/environment.rb +1 -1
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +21 -12
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +49 -34
- data/sandbox/itsi_sandbox_rails/config/environments/test.rb +19 -5
- data/sandbox/itsi_sandbox_rails/config/initializers/assets.rb +5 -0
- data/sandbox/itsi_sandbox_rails/config/initializers/filter_parameter_logging.rb +1 -1
- data/sandbox/itsi_sandbox_rails/config/initializers/permissions_policy.rb +13 -0
- data/sandbox/itsi_sandbox_rails/config/puma.rb +2 -9
- data/sandbox/itsi_sandbox_rails/config.ru +0 -1
- data/sandbox/itsi_sandbox_rails/db/migrate/20250301041554_create_posts.rb +1 -1
- data/sandbox/itsi_sandbox_rails/db/schema.rb +2 -2
- data/sandbox/itsi_sandbox_rails/lib/assets/.keep +0 -0
- data/sandbox/itsi_sandbox_rails/public/404.html +66 -113
- data/sandbox/itsi_sandbox_rails/public/406-unsupported-browser.html +65 -113
- data/sandbox/itsi_sandbox_rails/public/422.html +66 -113
- data/sandbox/itsi_sandbox_rails/public/500.html +65 -113
- data/sandbox/itsi_sandbox_rails/public/icon.png +0 -0
- data/sandbox/itsi_sandbox_rails/public/icon.svg +2 -2
- data/sandbox/itsi_sandbox_rails/test/channels/application_cable/connection_test.rb +13 -0
- data/sandbox/itsi_sandbox_roda/Gemfile.lock +3 -10
- data/tasks.txt +72 -35
- metadata +89 -139
- data/crates/itsi_server/src/body_proxy/mod.rs +0 -2
- data/crates/itsi_server/src/request/itsi_request.rs +0 -298
- data/crates/itsi_server/src/request/mod.rs +0 -1
- data/crates/itsi_server/src/response/mod.rs +0 -1
- data/crates/itsi_server/src/server/itsi_server.rs +0 -288
- data/gems/scheduler/ext/itsi_error/Cargo.lock +0 -368
- data/gems/scheduler/ext/itsi_error/Cargo.toml +0 -11
- data/gems/scheduler/ext/itsi_error/src/from.rs +0 -68
- data/gems/scheduler/ext/itsi_error/src/lib.rs +0 -24
- data/gems/scheduler/ext/itsi_instrument_entry/Cargo.toml +0 -15
- data/gems/scheduler/ext/itsi_instrument_entry/src/lib.rs +0 -31
- data/gems/scheduler/ext/itsi_rb_helpers/Cargo.lock +0 -355
- data/gems/scheduler/ext/itsi_rb_helpers/Cargo.toml +0 -10
- data/gems/scheduler/ext/itsi_rb_helpers/src/heap_value.rs +0 -121
- data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +0 -201
- data/gems/scheduler/ext/itsi_scheduler/Cargo.toml +0 -24
- data/gems/scheduler/ext/itsi_scheduler/extconf.rb +0 -6
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +0 -56
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +0 -44
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +0 -44
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler.rs +0 -308
- data/gems/scheduler/ext/itsi_scheduler/src/lib.rs +0 -38
- data/gems/scheduler/ext/itsi_server/Cargo.lock +0 -2956
- data/gems/scheduler/ext/itsi_server/Cargo.toml +0 -50
- data/gems/scheduler/ext/itsi_server/extconf.rb +0 -6
- data/gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs +0 -104
- data/gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +0 -122
- data/gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/gems/scheduler/ext/itsi_server/src/env.rs +0 -43
- data/gems/scheduler/ext/itsi_server/src/lib.rs +0 -180
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +0 -298
- data/gems/scheduler/ext/itsi_server/src/request/mod.rs +0 -1
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +0 -357
- data/gems/scheduler/ext/itsi_server/src/response/mod.rs +0 -1
- data/gems/scheduler/ext/itsi_server/src/server/bind.rs +0 -174
- data/gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs +0 -37
- data/gems/scheduler/ext/itsi_server/src/server/io_stream.rs +0 -104
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +0 -288
- data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +0 -9
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +0 -318
- data/gems/scheduler/ext/itsi_server/src/server/mod.rs +0 -11
- data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +0 -203
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +0 -260
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/mod.rs +0 -27
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +0 -276
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +0 -74
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +0 -399
- data/gems/scheduler/ext/itsi_server/src/server/tls/locked_dir_cache.rs +0 -132
- data/gems/scheduler/ext/itsi_server/src/server/tls.rs +0 -265
- data/gems/scheduler/ext/itsi_tracing/Cargo.lock +0 -274
- data/gems/scheduler/ext/itsi_tracing/Cargo.toml +0 -16
- data/gems/scheduler/ext/itsi_tracing/src/lib.rs +0 -58
- data/gems/server/ext/itsi_error/Cargo.lock +0 -368
- data/gems/server/ext/itsi_error/Cargo.toml +0 -11
- data/gems/server/ext/itsi_error/src/from.rs +0 -68
- data/gems/server/ext/itsi_error/src/lib.rs +0 -24
- data/gems/server/ext/itsi_instrument_entry/Cargo.toml +0 -15
- data/gems/server/ext/itsi_instrument_entry/src/lib.rs +0 -31
- data/gems/server/ext/itsi_rb_helpers/Cargo.lock +0 -355
- data/gems/server/ext/itsi_rb_helpers/Cargo.toml +0 -10
- data/gems/server/ext/itsi_rb_helpers/src/heap_value.rs +0 -121
- data/gems/server/ext/itsi_rb_helpers/src/lib.rs +0 -201
- data/gems/server/ext/itsi_scheduler/Cargo.toml +0 -24
- data/gems/server/ext/itsi_scheduler/extconf.rb +0 -6
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +0 -56
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +0 -44
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +0 -44
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler.rs +0 -308
- data/gems/server/ext/itsi_scheduler/src/lib.rs +0 -38
- data/gems/server/ext/itsi_server/Cargo.lock +0 -2956
- data/gems/server/ext/itsi_server/Cargo.toml +0 -50
- data/gems/server/ext/itsi_server/extconf.rb +0 -6
- data/gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs +0 -104
- data/gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +0 -122
- data/gems/server/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/gems/server/ext/itsi_server/src/env.rs +0 -43
- data/gems/server/ext/itsi_server/src/lib.rs +0 -180
- data/gems/server/ext/itsi_server/src/request/mod.rs +0 -1
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +0 -357
- data/gems/server/ext/itsi_server/src/response/mod.rs +0 -1
- data/gems/server/ext/itsi_server/src/server/bind.rs +0 -174
- data/gems/server/ext/itsi_server/src/server/bind_protocol.rs +0 -37
- data/gems/server/ext/itsi_server/src/server/io_stream.rs +0 -104
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +0 -288
- data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +0 -9
- data/gems/server/ext/itsi_server/src/server/listener.rs +0 -318
- data/gems/server/ext/itsi_server/src/server/mod.rs +0 -11
- data/gems/server/ext/itsi_server/src/server/process_worker.rs +0 -203
- data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +0 -260
- data/gems/server/ext/itsi_server/src/server/serve_strategy/mod.rs +0 -27
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +0 -276
- data/gems/server/ext/itsi_server/src/server/signal.rs +0 -74
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +0 -399
- data/gems/server/ext/itsi_server/src/server/tls/locked_dir_cache.rs +0 -132
- data/gems/server/ext/itsi_server/src/server/tls.rs +0 -265
- data/gems/server/ext/itsi_tracing/Cargo.lock +0 -274
- data/gems/server/ext/itsi_tracing/Cargo.toml +0 -16
- data/gems/server/ext/itsi_tracing/src/lib.rs +0 -58
- data/gems/server/lib/itsi/server/options_dsl.rb +0 -401
- data/gems/server/lib/itsi/stream_io.rb +0 -38
- data/location_dsl.rb +0 -381
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/docker-setup.sample +0 -3
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-app-boot.sample +0 -3
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-deploy.sample +0 -14
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/post-proxy-reboot.sample +0 -3
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-app-boot.sample +0 -3
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-build.sample +0 -51
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-connect.sample +0 -47
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-deploy.sample +0 -109
- data/sandbox/itsi_sandbox_rails/.kamal/hooks/pre-proxy-reboot.sample +0 -3
- data/sandbox/itsi_sandbox_rails/.kamal/secrets +0 -17
- data/sandbox/itsi_sandbox_rails/bin/dev +0 -2
- data/sandbox/itsi_sandbox_rails/bin/jobs +0 -6
- data/sandbox/itsi_sandbox_rails/bin/kamal +0 -27
- data/sandbox/itsi_sandbox_rails/bin/thrust +0 -5
- data/sandbox/itsi_sandbox_rails/config/cache.yml +0 -16
- data/sandbox/itsi_sandbox_rails/config/deploy.yml +0 -116
- data/sandbox/itsi_sandbox_rails/config/queue.yml +0 -18
- data/sandbox/itsi_sandbox_rails/config/recurring.yml +0 -10
- data/sandbox/itsi_sandbox_rails/db/cable_schema.rb +0 -11
- data/sandbox/itsi_sandbox_rails/db/cache_schema.rb +0 -14
- data/sandbox/itsi_sandbox_rails/db/queue_schema.rb +0 -129
- data/sandbox/itsi_sandbox_rails/public/400.html +0 -114
- data/sandbox/itsi_sandbox_rails/test/fixtures/posts.yml +0 -9
- data/sandbox/itsi_sandbox_rails/test/models/post_test.rb +0 -7
- /data/{sandbox/itsi_sandbox_rails/script/.keep → crates/_index.md} +0 -0
- /data/gems/server/lib/itsi/{index.html → server/default_app/index.html} +0 -0
@@ -0,0 +1,300 @@
|
|
1
|
+
use super::{
|
2
|
+
header_interpretation::{find_first_supported, header_contains},
|
3
|
+
FromValue, MiddlewareLayer,
|
4
|
+
};
|
5
|
+
use crate::server::{
|
6
|
+
itsi_service::RequestContext,
|
7
|
+
types::{HttpRequest, HttpResponse},
|
8
|
+
};
|
9
|
+
use async_compression::{
|
10
|
+
tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder},
|
11
|
+
Level,
|
12
|
+
};
|
13
|
+
use async_trait::async_trait;
|
14
|
+
use bytes::{Bytes, BytesMut};
|
15
|
+
use either::Either;
|
16
|
+
use futures::TryStreamExt;
|
17
|
+
use http::{
|
18
|
+
header::{GetAll, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
|
19
|
+
HeaderValue, Response,
|
20
|
+
};
|
21
|
+
use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
|
22
|
+
use hyper::body::{Body, Frame};
|
23
|
+
use magnus::error::Result;
|
24
|
+
use serde::{Deserialize, Serialize};
|
25
|
+
use std::convert::Infallible;
|
26
|
+
use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
|
27
|
+
use tokio_stream::StreamExt;
|
28
|
+
use tokio_util::io::{ReaderStream, StreamReader};
|
29
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
30
|
+
pub struct Compression {
|
31
|
+
min_size: usize,
|
32
|
+
algorithms: Vec<CompressionAlgorithm>,
|
33
|
+
compress_streams: bool,
|
34
|
+
mime_types: Vec<MimeType>,
|
35
|
+
level: CompressionLevel,
|
36
|
+
}
|
37
|
+
|
38
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
39
|
+
enum CompressionLevel {
|
40
|
+
#[serde(rename(deserialize = "fastest"))]
|
41
|
+
Fastest,
|
42
|
+
#[serde(rename(deserialize = "best"))]
|
43
|
+
Best,
|
44
|
+
#[serde(rename(deserialize = "default"))]
|
45
|
+
Default,
|
46
|
+
#[serde(rename(deserialize = "precise"))]
|
47
|
+
Precise(i32),
|
48
|
+
}
|
49
|
+
|
50
|
+
impl CompressionLevel {
|
51
|
+
fn to_async_compression_level(&self) -> Level {
|
52
|
+
match self {
|
53
|
+
CompressionLevel::Fastest => Level::Fastest,
|
54
|
+
CompressionLevel::Best => Level::Best,
|
55
|
+
CompressionLevel::Default => Level::Default,
|
56
|
+
CompressionLevel::Precise(level) => Level::Precise(*level),
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
62
|
+
pub enum CompressionAlgorithm {
|
63
|
+
#[serde(rename(deserialize = "gzip"))]
|
64
|
+
Gzip,
|
65
|
+
#[serde(rename(deserialize = "brotli"))]
|
66
|
+
Brotli,
|
67
|
+
#[serde(rename(deserialize = "deflate"))]
|
68
|
+
Deflate,
|
69
|
+
#[serde(rename(deserialize = "zstd"))]
|
70
|
+
Zstd,
|
71
|
+
#[serde(rename(deserialize = "none"))]
|
72
|
+
None,
|
73
|
+
}
|
74
|
+
|
75
|
+
impl CompressionAlgorithm {
|
76
|
+
pub fn as_str(&self) -> &'static str {
|
77
|
+
match self {
|
78
|
+
CompressionAlgorithm::Gzip => "gzip",
|
79
|
+
CompressionAlgorithm::Brotli => "br",
|
80
|
+
CompressionAlgorithm::Deflate => "deflate",
|
81
|
+
CompressionAlgorithm::Zstd => "zstd",
|
82
|
+
CompressionAlgorithm::None => "none",
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
pub fn header_value(&self) -> HeaderValue {
|
87
|
+
HeaderValue::from_str(self.as_str()).unwrap()
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
92
|
+
enum MimeType {
|
93
|
+
#[serde(rename(deserialize = "text"))]
|
94
|
+
Text,
|
95
|
+
#[serde(rename(deserialize = "image"))]
|
96
|
+
Image,
|
97
|
+
#[serde(rename(deserialize = "application"))]
|
98
|
+
Application,
|
99
|
+
#[serde(rename(deserialize = "audio"))]
|
100
|
+
Audio,
|
101
|
+
#[serde(rename(deserialize = "video"))]
|
102
|
+
Video,
|
103
|
+
#[serde(rename(deserialize = "other"))]
|
104
|
+
Other(String),
|
105
|
+
#[serde(rename(deserialize = "all"))]
|
106
|
+
All,
|
107
|
+
}
|
108
|
+
|
109
|
+
impl MimeType {
|
110
|
+
pub fn matches(&self, content_encodings: &GetAll<HeaderValue>) -> bool {
|
111
|
+
match self {
|
112
|
+
MimeType::Text => header_contains(content_encodings, "text/*"),
|
113
|
+
MimeType::Image => header_contains(content_encodings, "image/*"),
|
114
|
+
MimeType::Application => header_contains(content_encodings, "application/*"),
|
115
|
+
MimeType::Audio => header_contains(content_encodings, "audio/*"),
|
116
|
+
MimeType::Video => header_contains(content_encodings, "video/*"),
|
117
|
+
MimeType::Other(v) => header_contains(content_encodings, v),
|
118
|
+
MimeType::All => header_contains(content_encodings, "*"),
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
fn stream_encode<R>(encoder: R) -> BoxBody<Bytes, Infallible>
|
124
|
+
where
|
125
|
+
R: AsyncRead + Unpin + Sync + Send + 'static,
|
126
|
+
{
|
127
|
+
let encoded_stream = ReaderStream::new(encoder).map(|res| {
|
128
|
+
res.map(Frame::data)
|
129
|
+
.map_err(|_| -> Infallible { unreachable!("We handle IO errors above") })
|
130
|
+
});
|
131
|
+
BoxBody::new(StreamBody::new(encoded_stream))
|
132
|
+
}
|
133
|
+
|
134
|
+
fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: HeaderValue) {
|
135
|
+
if let Some(existing) = parts.headers.get(CONTENT_ENCODING) {
|
136
|
+
let mut encodings = existing.to_str().unwrap_or("").to_owned();
|
137
|
+
if !encodings.is_empty() {
|
138
|
+
encodings.push_str(", ");
|
139
|
+
}
|
140
|
+
encodings.push_str(new_encoding.to_str().unwrap());
|
141
|
+
parts
|
142
|
+
.headers
|
143
|
+
.insert(CONTENT_ENCODING, HeaderValue::from_str(&encodings).unwrap());
|
144
|
+
} else {
|
145
|
+
parts.headers.insert(CONTENT_ENCODING, new_encoding);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
#[async_trait]
|
150
|
+
impl MiddlewareLayer for Compression {
|
151
|
+
/// A the request comes in, take note of the accepted content encodings,
|
152
|
+
/// so that we can apply compression on the response, where appropriate.
|
153
|
+
///
|
154
|
+
/// We store the temporary state inside the RequestContext.
|
155
|
+
async fn before(
|
156
|
+
&self,
|
157
|
+
req: HttpRequest,
|
158
|
+
context: &mut RequestContext,
|
159
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
160
|
+
let algo = match find_first_supported(
|
161
|
+
&req.headers().get_all(ACCEPT_ENCODING),
|
162
|
+
self.algorithms.iter().map(|algo| algo.as_str()),
|
163
|
+
) {
|
164
|
+
Some("gzip") => CompressionAlgorithm::Gzip,
|
165
|
+
Some("br") => CompressionAlgorithm::Brotli,
|
166
|
+
Some("deflate") => CompressionAlgorithm::Deflate,
|
167
|
+
Some("zstd") => CompressionAlgorithm::Zstd,
|
168
|
+
_ => CompressionAlgorithm::None,
|
169
|
+
};
|
170
|
+
|
171
|
+
if matches!(algo, CompressionAlgorithm::None) {
|
172
|
+
return Ok(Either::Left(req));
|
173
|
+
}
|
174
|
+
|
175
|
+
context.set_compression_method(algo);
|
176
|
+
|
177
|
+
Ok(Either::Left(req))
|
178
|
+
}
|
179
|
+
|
180
|
+
/// We'll apply compression on the response, where appropriate.
|
181
|
+
/// This is if:
|
182
|
+
/// * The response body is larger than the minimum size.
|
183
|
+
/// * The response content type is supported.
|
184
|
+
/// * The client supports the compression algorithm.
|
185
|
+
async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
186
|
+
let compression_method;
|
187
|
+
if let Some(method) = context.compression_method.get() {
|
188
|
+
compression_method = method.clone();
|
189
|
+
} else {
|
190
|
+
return resp;
|
191
|
+
}
|
192
|
+
|
193
|
+
if matches!(compression_method, CompressionAlgorithm::None) {
|
194
|
+
return resp;
|
195
|
+
}
|
196
|
+
|
197
|
+
let body_size = resp.size_hint().exact();
|
198
|
+
let resp = resp;
|
199
|
+
|
200
|
+
if !self
|
201
|
+
.mime_types
|
202
|
+
.iter()
|
203
|
+
.any(|mt| mt.matches(&resp.headers().get_all(CONTENT_TYPE)))
|
204
|
+
{
|
205
|
+
return resp;
|
206
|
+
}
|
207
|
+
|
208
|
+
if body_size.is_none() && !self.compress_streams {
|
209
|
+
return resp;
|
210
|
+
}
|
211
|
+
|
212
|
+
if body_size.is_some_and(|s| s < self.min_size as u64) {
|
213
|
+
return resp;
|
214
|
+
}
|
215
|
+
|
216
|
+
let (mut parts, body) = resp.into_parts();
|
217
|
+
|
218
|
+
let new_body = if let Some(_size) = body_size {
|
219
|
+
let full_bytes: Bytes = body
|
220
|
+
.into_data_stream()
|
221
|
+
.try_fold(BytesMut::new(), |mut acc, chunk| async move {
|
222
|
+
acc.extend_from_slice(&chunk);
|
223
|
+
Ok(acc)
|
224
|
+
})
|
225
|
+
.await
|
226
|
+
.unwrap()
|
227
|
+
.freeze();
|
228
|
+
|
229
|
+
let cursor = std::io::Cursor::new(full_bytes);
|
230
|
+
let reader = BufReader::new(cursor);
|
231
|
+
let compressed_bytes = match compression_method {
|
232
|
+
CompressionAlgorithm::Gzip => {
|
233
|
+
let mut encoder =
|
234
|
+
GzipEncoder::with_quality(reader, self.level.to_async_compression_level());
|
235
|
+
let mut buf = Vec::new();
|
236
|
+
encoder.read_to_end(&mut buf).await.unwrap();
|
237
|
+
buf
|
238
|
+
}
|
239
|
+
CompressionAlgorithm::Brotli => {
|
240
|
+
let mut encoder = BrotliEncoder::with_quality(
|
241
|
+
reader,
|
242
|
+
self.level.to_async_compression_level(),
|
243
|
+
);
|
244
|
+
let mut buf = Vec::new();
|
245
|
+
encoder.read_to_end(&mut buf).await.unwrap();
|
246
|
+
buf
|
247
|
+
}
|
248
|
+
CompressionAlgorithm::Deflate => {
|
249
|
+
let mut encoder = DeflateEncoder::with_quality(
|
250
|
+
reader,
|
251
|
+
self.level.to_async_compression_level(),
|
252
|
+
);
|
253
|
+
let mut buf = Vec::new();
|
254
|
+
encoder.read_to_end(&mut buf).await.unwrap();
|
255
|
+
buf
|
256
|
+
}
|
257
|
+
CompressionAlgorithm::Zstd => {
|
258
|
+
let mut encoder =
|
259
|
+
ZstdEncoder::with_quality(reader, self.level.to_async_compression_level());
|
260
|
+
let mut buf = Vec::new();
|
261
|
+
encoder.read_to_end(&mut buf).await.unwrap();
|
262
|
+
buf
|
263
|
+
}
|
264
|
+
CompressionAlgorithm::None => unreachable!(),
|
265
|
+
};
|
266
|
+
BoxBody::new(Full::new(Bytes::from(compressed_bytes)))
|
267
|
+
} else {
|
268
|
+
let stream = body
|
269
|
+
.into_data_stream()
|
270
|
+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
|
271
|
+
let async_read_fut = StreamReader::new(stream);
|
272
|
+
let reader = BufReader::new(async_read_fut);
|
273
|
+
match compression_method {
|
274
|
+
CompressionAlgorithm::Gzip => stream_encode(GzipEncoder::with_quality(
|
275
|
+
reader,
|
276
|
+
self.level.to_async_compression_level(),
|
277
|
+
)),
|
278
|
+
CompressionAlgorithm::Brotli => stream_encode(BrotliEncoder::with_quality(
|
279
|
+
reader,
|
280
|
+
self.level.to_async_compression_level(),
|
281
|
+
)),
|
282
|
+
CompressionAlgorithm::Deflate => stream_encode(DeflateEncoder::with_quality(
|
283
|
+
reader,
|
284
|
+
self.level.to_async_compression_level(),
|
285
|
+
)),
|
286
|
+
CompressionAlgorithm::Zstd => stream_encode(ZstdEncoder::with_quality(
|
287
|
+
reader,
|
288
|
+
self.level.to_async_compression_level(),
|
289
|
+
)),
|
290
|
+
CompressionAlgorithm::None => unreachable!(),
|
291
|
+
}
|
292
|
+
};
|
293
|
+
|
294
|
+
update_content_encoding(&mut parts, compression_method.header_value());
|
295
|
+
parts.headers.remove(CONTENT_LENGTH);
|
296
|
+
|
297
|
+
Response::from_parts(parts, new_body)
|
298
|
+
}
|
299
|
+
}
|
300
|
+
impl FromValue for Compression {}
|
@@ -0,0 +1,287 @@
|
|
1
|
+
use super::{FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
5
|
+
};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use http::{HeaderMap, Method, Response};
|
8
|
+
use http_body_util::{combinators::BoxBody, Empty};
|
9
|
+
use itsi_error::ItsiError;
|
10
|
+
use magnus::error::Result;
|
11
|
+
use serde::Deserialize;
|
12
|
+
|
13
|
+
#[derive(Debug, Clone, Deserialize)]
|
14
|
+
pub struct Cors {
|
15
|
+
pub allowed_origins: Vec<String>,
|
16
|
+
pub allowed_methods: Vec<HttpMethod>,
|
17
|
+
pub allowed_headers: Vec<String>,
|
18
|
+
pub exposed_headers: Vec<String>,
|
19
|
+
pub allow_credentials: bool,
|
20
|
+
pub max_age: Option<u64>,
|
21
|
+
}
|
22
|
+
|
23
|
+
#[derive(Debug, Clone, Deserialize)]
|
24
|
+
pub enum HttpMethod {
|
25
|
+
#[serde(rename(deserialize = "GET"))]
|
26
|
+
Get,
|
27
|
+
#[serde(rename(deserialize = "POST"))]
|
28
|
+
Post,
|
29
|
+
#[serde(rename(deserialize = "PUT"))]
|
30
|
+
Put,
|
31
|
+
#[serde(rename(deserialize = "DELETE"))]
|
32
|
+
Delete,
|
33
|
+
#[serde(rename(deserialize = "OPTIONS"))]
|
34
|
+
Options,
|
35
|
+
#[serde(rename(deserialize = "HEAD"))]
|
36
|
+
Head,
|
37
|
+
#[serde(rename(deserialize = "PATCH"))]
|
38
|
+
Patch,
|
39
|
+
}
|
40
|
+
|
41
|
+
impl HttpMethod {
|
42
|
+
pub fn matches(&self, other: &str) -> bool {
|
43
|
+
match self {
|
44
|
+
HttpMethod::Get => other.eq_ignore_ascii_case("GET"),
|
45
|
+
HttpMethod::Post => other.eq_ignore_ascii_case("POST"),
|
46
|
+
HttpMethod::Put => other.eq_ignore_ascii_case("PUT"),
|
47
|
+
HttpMethod::Delete => other.eq_ignore_ascii_case("DELETE"),
|
48
|
+
HttpMethod::Options => other.eq_ignore_ascii_case("OPTIONS"),
|
49
|
+
HttpMethod::Head => other.eq_ignore_ascii_case("HEAD"),
|
50
|
+
HttpMethod::Patch => other.eq_ignore_ascii_case("PATCH"),
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
pub fn to_str(&self) -> &str {
|
55
|
+
match self {
|
56
|
+
HttpMethod::Get => "GET",
|
57
|
+
HttpMethod::Post => "POST",
|
58
|
+
HttpMethod::Put => "PUT",
|
59
|
+
HttpMethod::Delete => "DELETE",
|
60
|
+
HttpMethod::Options => "OPTIONS",
|
61
|
+
HttpMethod::Head => "HEAD",
|
62
|
+
HttpMethod::Patch => "PATCH",
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
impl Cors {
|
68
|
+
/// Generate the simple CORS headers (used in normal responses)
|
69
|
+
fn cors_headers(&self, origin: &str) -> Result<HeaderMap> {
|
70
|
+
let mut headers = HeaderMap::new();
|
71
|
+
|
72
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
|
73
|
+
|
74
|
+
if origin.is_empty() {
|
75
|
+
// When credentials are allowed, you cannot return "*".
|
76
|
+
if !self.allow_credentials {
|
77
|
+
headers.insert(
|
78
|
+
"Access-Control-Allow-Origin",
|
79
|
+
"*".parse().map_err(ItsiError::default)?,
|
80
|
+
);
|
81
|
+
}
|
82
|
+
return Ok(headers);
|
83
|
+
}
|
84
|
+
|
85
|
+
// Only return a header if the origin is allowed.
|
86
|
+
if self.allowed_origins.iter().any(|o| o == origin || o == "*") {
|
87
|
+
// If credentials are allowed, we must echo back the exact origin.
|
88
|
+
let value = if self.allow_credentials {
|
89
|
+
origin
|
90
|
+
} else {
|
91
|
+
// If not, and if "*" is allowed, you can still use "*".
|
92
|
+
if self.allowed_origins.iter().any(|o| o == "*") {
|
93
|
+
"*"
|
94
|
+
} else {
|
95
|
+
origin
|
96
|
+
}
|
97
|
+
};
|
98
|
+
headers.insert(
|
99
|
+
"Access-Control-Allow-Origin",
|
100
|
+
value.parse().map_err(ItsiError::default)?,
|
101
|
+
);
|
102
|
+
}
|
103
|
+
|
104
|
+
if !self.allowed_methods.is_empty() {
|
105
|
+
headers.insert(
|
106
|
+
"Access-Control-Allow-Methods",
|
107
|
+
self.allowed_methods
|
108
|
+
.iter()
|
109
|
+
.map(HttpMethod::to_str)
|
110
|
+
.collect::<Vec<&str>>()
|
111
|
+
.join(", ")
|
112
|
+
.parse()
|
113
|
+
.map_err(ItsiError::default)?,
|
114
|
+
);
|
115
|
+
}
|
116
|
+
if !self.allowed_headers.is_empty() {
|
117
|
+
headers.insert(
|
118
|
+
"Access-Control-Allow-Headers",
|
119
|
+
self.allowed_headers
|
120
|
+
.join(", ")
|
121
|
+
.parse()
|
122
|
+
.map_err(ItsiError::default)?,
|
123
|
+
);
|
124
|
+
}
|
125
|
+
if self.allow_credentials {
|
126
|
+
headers.insert(
|
127
|
+
"Access-Control-Allow-Credentials",
|
128
|
+
"true".parse().map_err(ItsiError::default)?,
|
129
|
+
);
|
130
|
+
}
|
131
|
+
if let Some(max_age) = self.max_age {
|
132
|
+
headers.insert(
|
133
|
+
"Access-Control-Max-Age",
|
134
|
+
max_age.to_string().parse().map_err(ItsiError::default)?,
|
135
|
+
);
|
136
|
+
}
|
137
|
+
if !self.exposed_headers.is_empty() {
|
138
|
+
headers.insert(
|
139
|
+
"Access-Control-Expose-Headers",
|
140
|
+
self.exposed_headers
|
141
|
+
.join(", ")
|
142
|
+
.parse()
|
143
|
+
.map_err(ItsiError::default)?,
|
144
|
+
);
|
145
|
+
}
|
146
|
+
Ok(headers)
|
147
|
+
}
|
148
|
+
|
149
|
+
fn preflight_headers(
|
150
|
+
&self,
|
151
|
+
origin: Option<&str>,
|
152
|
+
req_method: Option<&str>,
|
153
|
+
req_headers: Option<&str>,
|
154
|
+
) -> Result<HeaderMap> {
|
155
|
+
let mut headers = HeaderMap::new();
|
156
|
+
|
157
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
|
158
|
+
|
159
|
+
let origin = match origin {
|
160
|
+
Some(o) if !o.is_empty() => o,
|
161
|
+
_ => return Ok(headers), // Missing Origin – preflight fails
|
162
|
+
};
|
163
|
+
|
164
|
+
if !self
|
165
|
+
.allowed_origins
|
166
|
+
.iter()
|
167
|
+
.any(|allowed| allowed == "*" || allowed == origin)
|
168
|
+
{
|
169
|
+
return Ok(headers);
|
170
|
+
}
|
171
|
+
|
172
|
+
let request_method = match req_method {
|
173
|
+
Some(m) if !m.is_empty() => m,
|
174
|
+
_ => return Ok(headers), // Missing request method – preflight fails
|
175
|
+
};
|
176
|
+
|
177
|
+
if !self
|
178
|
+
.allowed_methods
|
179
|
+
.iter()
|
180
|
+
.any(|m| m.matches(request_method))
|
181
|
+
{
|
182
|
+
return Ok(headers);
|
183
|
+
}
|
184
|
+
|
185
|
+
if let Some(request_headers) = req_headers {
|
186
|
+
let req_headers_list: Vec<&str> = request_headers
|
187
|
+
.split(',')
|
188
|
+
.map(|s| s.trim())
|
189
|
+
.filter(|s| !s.is_empty())
|
190
|
+
.collect();
|
191
|
+
for header in req_headers_list {
|
192
|
+
if !self
|
193
|
+
.allowed_headers
|
194
|
+
.iter()
|
195
|
+
.any(|allowed| allowed.eq_ignore_ascii_case(header))
|
196
|
+
{
|
197
|
+
return Ok(headers);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
headers.insert("Access-Control-Allow-Origin", origin.parse().unwrap());
|
203
|
+
headers.insert(
|
204
|
+
"Access-Control-Allow-Methods",
|
205
|
+
self.allowed_methods
|
206
|
+
.iter()
|
207
|
+
.map(HttpMethod::to_str)
|
208
|
+
.collect::<Vec<&str>>()
|
209
|
+
.join(", ")
|
210
|
+
.parse()
|
211
|
+
.map_err(ItsiError::default)?,
|
212
|
+
);
|
213
|
+
headers.insert(
|
214
|
+
"Access-Control-Allow-Headers",
|
215
|
+
self.allowed_headers
|
216
|
+
.join(", ")
|
217
|
+
.parse()
|
218
|
+
.map_err(ItsiError::default)?,
|
219
|
+
);
|
220
|
+
if self.allow_credentials {
|
221
|
+
headers.insert(
|
222
|
+
"Access-Control-Allow-Credentials",
|
223
|
+
"true".parse().map_err(ItsiError::default)?,
|
224
|
+
);
|
225
|
+
}
|
226
|
+
if let Some(max_age) = self.max_age {
|
227
|
+
headers.insert(
|
228
|
+
"Access-Control-Max-Age",
|
229
|
+
max_age.to_string().parse().map_err(ItsiError::default)?,
|
230
|
+
);
|
231
|
+
}
|
232
|
+
if !self.exposed_headers.is_empty() {
|
233
|
+
headers.insert(
|
234
|
+
"Access-Control-Expose-Headers",
|
235
|
+
self.exposed_headers
|
236
|
+
.join(", ")
|
237
|
+
.parse()
|
238
|
+
.map_err(ItsiError::default)?,
|
239
|
+
);
|
240
|
+
}
|
241
|
+
|
242
|
+
Ok(headers)
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
#[async_trait]
|
247
|
+
impl MiddlewareLayer for Cors {
|
248
|
+
// For OPTIONS (preflight) requests we:
|
249
|
+
// 1. Extract Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
|
250
|
+
// 2. Validate them using our hardened preflight_headers function.
|
251
|
+
// 3. If validations pass (i.e. headers is non-empty), return a 204 response with those headers.
|
252
|
+
// Otherwise, the absence of headers indicates the request doesn’t meet the CORS policy.
|
253
|
+
async fn before(
|
254
|
+
&self,
|
255
|
+
req: HttpRequest,
|
256
|
+
context: &mut RequestContext,
|
257
|
+
) -> Result<either::Either<HttpRequest, HttpResponse>> {
|
258
|
+
let origin = req.header("Origin");
|
259
|
+
if req.method() == Method::OPTIONS {
|
260
|
+
let ac_request_method = req.header("Access-Control-Request-Method");
|
261
|
+
let ac_request_headers = req.header("Access-Control-Request-Headers");
|
262
|
+
let headers = self.preflight_headers(origin, ac_request_method, ac_request_headers)?;
|
263
|
+
|
264
|
+
let mut response_builder = Response::builder().status(204);
|
265
|
+
*response_builder.headers_mut().unwrap() = headers;
|
266
|
+
let response = response_builder
|
267
|
+
.body(BoxBody::new(Empty::new()))
|
268
|
+
.map_err(ItsiError::default)?;
|
269
|
+
return Ok(either::Either::Right(response));
|
270
|
+
}
|
271
|
+
context.set_origin(origin.map(|s| s.to_string()));
|
272
|
+
Ok(either::Either::Left(req))
|
273
|
+
}
|
274
|
+
|
275
|
+
// The after hook can be used to inject CORS headers into non-preflight responses.
|
276
|
+
async fn after(&self, mut resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
277
|
+
if let Some(Some(origin)) = context.origin.get() {
|
278
|
+
if let Ok(cors_headers) = self.cors_headers(origin) {
|
279
|
+
for (key, value) in cors_headers.iter() {
|
280
|
+
resp.headers_mut().insert(key.clone(), value.clone());
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
resp
|
285
|
+
}
|
286
|
+
}
|
287
|
+
impl FromValue for Cors {}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
use crate::server::{
|
2
|
+
itsi_service::RequestContext,
|
3
|
+
types::{HttpRequest, HttpResponse},
|
4
|
+
};
|
5
|
+
|
6
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
7
|
+
use async_trait::async_trait;
|
8
|
+
use either::Either;
|
9
|
+
use itsi_error::ItsiError;
|
10
|
+
use magnus::error::Result;
|
11
|
+
use regex::RegexSet;
|
12
|
+
use serde::Deserialize;
|
13
|
+
use std::sync::OnceLock;
|
14
|
+
|
15
|
+
#[derive(Debug, Clone, Deserialize)]
|
16
|
+
pub struct DenyList {
|
17
|
+
#[serde(skip_deserializing)]
|
18
|
+
pub denied_ips: OnceLock<RegexSet>,
|
19
|
+
pub denied_patterns: Vec<String>,
|
20
|
+
pub error_response: ErrorResponse,
|
21
|
+
}
|
22
|
+
|
23
|
+
#[async_trait]
|
24
|
+
impl MiddlewareLayer for DenyList {
|
25
|
+
async fn initialize(&self) -> Result<()> {
|
26
|
+
let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::default)?;
|
27
|
+
self.denied_ips
|
28
|
+
.set(denied_ips)
|
29
|
+
.map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
|
30
|
+
Ok(())
|
31
|
+
}
|
32
|
+
|
33
|
+
async fn before(
|
34
|
+
&self,
|
35
|
+
req: HttpRequest,
|
36
|
+
context: &mut RequestContext,
|
37
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
38
|
+
if let Some(denied_ips) = self.denied_ips.get() {
|
39
|
+
if denied_ips.is_match(&context.addr) {
|
40
|
+
return Ok(Either::Right(
|
41
|
+
self.error_response.to_http_response(&req).await,
|
42
|
+
));
|
43
|
+
}
|
44
|
+
}
|
45
|
+
Ok(Either::Left(req))
|
46
|
+
}
|
47
|
+
}
|
48
|
+
impl FromValue for DenyList {}
|