itsi-scheduler 0.1.5 → 0.2.2

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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +120 -52
  3. data/README.md +57 -24
  4. data/Rakefile +0 -4
  5. data/ext/itsi_acme/Cargo.toml +86 -0
  6. data/ext/itsi_acme/examples/high_level.rs +63 -0
  7. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  8. data/ext/itsi_acme/examples/low_level.rs +87 -0
  9. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  10. data/ext/itsi_acme/src/acceptor.rs +81 -0
  11. data/ext/itsi_acme/src/acme.rs +354 -0
  12. data/ext/itsi_acme/src/axum.rs +86 -0
  13. data/ext/itsi_acme/src/cache.rs +39 -0
  14. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  15. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  16. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  17. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  18. data/ext/itsi_acme/src/caches/no.rs +78 -0
  19. data/ext/itsi_acme/src/caches/test.rs +136 -0
  20. data/ext/itsi_acme/src/config.rs +172 -0
  21. data/ext/itsi_acme/src/https_helper.rs +69 -0
  22. data/ext/itsi_acme/src/incoming.rs +142 -0
  23. data/ext/itsi_acme/src/jose.rs +161 -0
  24. data/ext/itsi_acme/src/lib.rs +142 -0
  25. data/ext/itsi_acme/src/resolver.rs +59 -0
  26. data/ext/itsi_acme/src/state.rs +424 -0
  27. data/ext/itsi_error/Cargo.toml +1 -0
  28. data/ext/itsi_error/src/lib.rs +106 -7
  29. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  30. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  31. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  32. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  33. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  34. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  35. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  36. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  37. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  38. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  39. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  40. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  41. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  42. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  43. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  44. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  45. data/ext/itsi_rb_helpers/src/lib.rs +63 -12
  46. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  47. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  48. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  49. 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
  50. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  51. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  52. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  53. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  54. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  55. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  56. data/ext/itsi_scheduler/Cargo.toml +1 -1
  57. data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
  58. data/ext/itsi_scheduler/src/lib.rs +1 -0
  59. data/ext/itsi_server/Cargo.lock +2956 -0
  60. data/ext/itsi_server/Cargo.toml +73 -29
  61. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  62. data/ext/itsi_server/src/env.rs +43 -0
  63. data/ext/itsi_server/src/lib.rs +114 -75
  64. data/ext/itsi_server/src/prelude.rs +2 -0
  65. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  66. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
  67. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  68. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  69. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
  70. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
  71. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
  72. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
  74. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  75. data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
  76. data/ext/itsi_server/src/server/binds/listener.rs +444 -0
  77. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  78. data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
  79. data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
  80. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  81. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  82. data/ext/itsi_server/src/server/io_stream.rs +2 -1
  83. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  112. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  113. data/ext/itsi_server/src/server/mod.rs +7 -5
  114. data/ext/itsi_server/src/server/process_worker.rs +65 -14
  115. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  116. data/ext/itsi_server/src/server/request_job.rs +11 -0
  117. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
  118. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  119. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
  120. data/ext/itsi_server/src/server/signal.rs +33 -26
  121. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  122. data/ext/itsi_server/src/server/thread_worker.rs +218 -107
  123. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  124. data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
  125. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  126. data/ext/itsi_server/src/services/mod.rs +6 -0
  127. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  128. data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
  129. data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
  130. data/ext/itsi_tracing/Cargo.toml +1 -0
  131. data/ext/itsi_tracing/src/lib.rs +362 -33
  132. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  133. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  134. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  135. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  136. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  137. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  138. data/itsi-scheduler-100.png +0 -0
  139. data/lib/itsi/scheduler/version.rb +1 -1
  140. data/lib/itsi/scheduler.rb +11 -6
  141. metadata +117 -24
  142. data/CHANGELOG.md +0 -5
  143. data/CODE_OF_CONDUCT.md +0 -132
  144. data/LICENSE.txt +0 -21
  145. data/ext/itsi_error/src/from.rs +0 -71
  146. data/ext/itsi_server/extconf.rb +0 -6
  147. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  148. data/ext/itsi_server/src/request/itsi_request.rs +0 -277
  149. data/ext/itsi_server/src/request/mod.rs +0 -1
  150. data/ext/itsi_server/src/response/mod.rs +0 -1
  151. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  152. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  153. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
  154. data/ext/itsi_server/src/server/listener.rs +0 -327
  155. /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,151 @@
1
+ use crate::{
2
+ server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
3
+ };
4
+
5
+ use super::{FromValue, MiddlewareLayer};
6
+ use async_trait::async_trait;
7
+ use http::{HeaderName, HeaderValue};
8
+ use magnus::error::Result;
9
+ use serde::Deserialize;
10
+ use std::{collections::HashMap, sync::OnceLock};
11
+ use tracing::debug;
12
+
13
+ #[derive(Debug, Deserialize)]
14
+ pub struct CacheControl {
15
+ #[serde(default)]
16
+ pub max_age: Option<u64>,
17
+ #[serde(default)]
18
+ pub s_max_age: Option<u64>,
19
+ #[serde(default)]
20
+ pub stale_while_revalidate: Option<u64>,
21
+ #[serde(default)]
22
+ pub stale_if_error: Option<u64>,
23
+ #[serde(default)]
24
+ pub public: bool,
25
+ #[serde(default)]
26
+ pub private: bool,
27
+ #[serde(default)]
28
+ pub no_cache: bool,
29
+ #[serde(default)]
30
+ pub no_store: bool,
31
+ #[serde(default)]
32
+ pub must_revalidate: bool,
33
+ #[serde(default)]
34
+ pub proxy_revalidate: bool,
35
+ #[serde(default)]
36
+ pub immutable: bool,
37
+ #[serde(default)]
38
+ pub vary: Vec<String>,
39
+ #[serde(default)]
40
+ pub additional_headers: HashMap<String, String>,
41
+ #[serde(skip_deserializing)]
42
+ pub cache_control_str: OnceLock<String>,
43
+ }
44
+
45
+ #[async_trait]
46
+ impl MiddlewareLayer for CacheControl {
47
+ async fn initialize(&self) -> Result<()> {
48
+ let mut directives = Vec::new();
49
+
50
+ if self.public && !self.private {
51
+ directives.push("public".to_owned());
52
+ } else if self.private && !self.public {
53
+ directives.push("private".to_owned());
54
+ }
55
+ if self.no_cache {
56
+ directives.push("no-cache".to_owned());
57
+ }
58
+ if self.no_store {
59
+ directives.push("no-store".to_owned());
60
+ }
61
+ if self.must_revalidate {
62
+ directives.push("must-revalidate".to_owned());
63
+ }
64
+ if self.proxy_revalidate {
65
+ directives.push("proxy-revalidate".to_owned());
66
+ }
67
+ if self.immutable {
68
+ directives.push("immutable".to_owned());
69
+ }
70
+
71
+ // Add age parameters
72
+ if let Some(max_age) = self.max_age {
73
+ directives.push(format!("max-age={}", max_age));
74
+ }
75
+
76
+ if let Some(s_max_age) = self.s_max_age {
77
+ directives.push(format!("s-maxage={}", s_max_age));
78
+ }
79
+
80
+ if let Some(stale_while_revalidate) = self.stale_while_revalidate {
81
+ directives.push(format!("stale-while-revalidate={}", stale_while_revalidate));
82
+ }
83
+
84
+ if let Some(stale_if_error) = self.stale_if_error {
85
+ directives.push(format!("stale-if-error={}", stale_if_error));
86
+ }
87
+
88
+ // Set the Cache-Control header if we have directives
89
+ if !directives.is_empty() {
90
+ let cache_control_value = directives.join(", ");
91
+ debug!(target: "middleware::cache_control", "Built cache-control directive {}", cache_control_value);
92
+ self.cache_control_str.set(cache_control_value).unwrap();
93
+ }
94
+
95
+ Ok(())
96
+ }
97
+
98
+ async fn after(&self, mut resp: HttpResponse, _: &mut HttpRequestContext) -> HttpResponse {
99
+ // Skip for statuses where caching doesn't make sense
100
+ let status = resp.status().as_u16();
101
+ if matches!(status, 401 | 403 | 500..=599) {
102
+ debug!(target: "middleware::cache_control", "Skipping cache-control for status {}", status);
103
+ return resp;
104
+ }
105
+
106
+ // Set the Cache-Control header if we have directives
107
+ if let Some(cache_control_value) = self.cache_control_str.get() {
108
+ if let Ok(value) = HeaderValue::from_str(cache_control_value) {
109
+ debug!(target: "middleware::cache_control", "Setting cache-control header to {}", cache_control_value);
110
+ resp.headers_mut().insert("Cache-Control", value);
111
+ } else {
112
+ debug!(target: "middleware::cache_control", "Failed to parse cache-control value {}", cache_control_value);
113
+ }
114
+ } else {
115
+ debug!(target: "middleware::cache_control", "No cache-control value provided");
116
+ }
117
+
118
+ // Set Expires header based on max-age if present
119
+ if let Some(max_age) = self.max_age {
120
+ // Set the Expires header based on max-age
121
+ // Use a helper to format the HTTP date correctly
122
+ debug!(target: "middleware::cache_control", "Setting expires header to {}", max_age);
123
+ let expires = chrono::Utc::now() + chrono::Duration::seconds(max_age as i64);
124
+ let expires_str = expires.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
125
+ if let Ok(value) = HeaderValue::from_str(&expires_str) {
126
+ resp.headers_mut().insert("Expires", value);
127
+ }
128
+ }
129
+
130
+ if !self.vary.is_empty() {
131
+ let vary_value = self.vary.join(", ");
132
+ if let Ok(value) = HeaderValue::from_str(&vary_value) {
133
+ resp.headers_mut().insert("Vary", value);
134
+ }
135
+ }
136
+
137
+ // Set additional custom headers
138
+ for (name, value) in &self.additional_headers {
139
+ if let Ok(header_value) = HeaderValue::from_str(value) {
140
+ if let Ok(header_name) = name.parse::<HeaderName>() {
141
+ debug!(target: "middleware::cache_control", "Setting custom header {} to {:?}", header_name, header_value);
142
+ resp.headers_mut().insert(header_name, header_value);
143
+ }
144
+ }
145
+ }
146
+
147
+ resp
148
+ }
149
+ }
150
+
151
+ impl FromValue for CacheControl {}
@@ -0,0 +1,316 @@
1
+ use crate::{
2
+ server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
3
+ };
4
+
5
+ use super::{
6
+ header_interpretation::{find_first_supported, header_contains},
7
+ FromValue, MiddlewareLayer,
8
+ };
9
+
10
+ use async_compression::{
11
+ tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder},
12
+ Level,
13
+ };
14
+ use async_trait::async_trait;
15
+ use bytes::{Bytes, BytesMut};
16
+ use futures::TryStreamExt;
17
+ use http::{
18
+ header::{GetAll, 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 serde::{Deserialize, Serialize};
24
+ use std::convert::Infallible;
25
+ use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
26
+ use tokio_stream::StreamExt;
27
+ use tokio_util::io::{ReaderStream, StreamReader};
28
+ use tracing::debug;
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 = "balanced"))]
45
+ Balanced,
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::Balanced => 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 = "br"))]
66
+ Brotli,
67
+ #[serde(rename(deserialize = "deflate"))]
68
+ Deflate,
69
+ #[serde(rename(deserialize = "zstd"))]
70
+ Zstd,
71
+ #[serde(rename(deserialize = "identity"))]
72
+ Identity,
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::Identity => "identity",
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 = "font"))]
104
+ Font,
105
+ #[serde(rename(deserialize = "other"))]
106
+ Other(String),
107
+ #[serde(rename(deserialize = "all"))]
108
+ All,
109
+ }
110
+
111
+ impl MimeType {
112
+ pub fn matches(&self, content_encodings: &GetAll<HeaderValue>) -> bool {
113
+ match self {
114
+ MimeType::Text => header_contains(content_encodings, "text/*"),
115
+ MimeType::Image => header_contains(content_encodings, "image/*"),
116
+ MimeType::Application => header_contains(content_encodings, "application/*"),
117
+ MimeType::Audio => header_contains(content_encodings, "audio/*"),
118
+ MimeType::Video => header_contains(content_encodings, "video/*"),
119
+ MimeType::Font => header_contains(content_encodings, "font/*"),
120
+ MimeType::Other(v) => header_contains(content_encodings, v),
121
+ MimeType::All => header_contains(content_encodings, "*"),
122
+ }
123
+ }
124
+ }
125
+
126
+ fn stream_encode<R>(encoder: R) -> BoxBody<Bytes, Infallible>
127
+ where
128
+ R: AsyncRead + Unpin + Sync + Send + 'static,
129
+ {
130
+ let encoded_stream = ReaderStream::new(encoder).map(|res| {
131
+ res.map(Frame::data)
132
+ .map_err(|_| -> Infallible { unreachable!("We handle IO errors above") })
133
+ });
134
+ BoxBody::new(StreamBody::new(encoded_stream))
135
+ }
136
+
137
+ fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: HeaderValue) {
138
+ if let Some(existing) = parts.headers.get(CONTENT_ENCODING) {
139
+ let mut encodings = existing.to_str().unwrap_or("").to_owned();
140
+ if !encodings.is_empty() {
141
+ encodings.push_str(", ");
142
+ }
143
+ encodings.push_str(new_encoding.to_str().unwrap());
144
+ parts
145
+ .headers
146
+ .insert(CONTENT_ENCODING, HeaderValue::from_str(&encodings).unwrap());
147
+ } else {
148
+ parts.headers.insert(CONTENT_ENCODING, new_encoding);
149
+ }
150
+ }
151
+
152
+ #[async_trait]
153
+ impl MiddlewareLayer for Compression {
154
+ /// We'll apply compression on the response, where appropriate.
155
+ /// This is if:
156
+ /// * The response body is larger than the minimum size.
157
+ /// * The response content type is supported.
158
+ /// * The client supports the compression algorithm.
159
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
160
+ let body_size = resp.size_hint().exact();
161
+ let resp = resp;
162
+
163
+ // Don't compress if it's not an explicitly listed compressable type
164
+ if !self
165
+ .mime_types
166
+ .iter()
167
+ .any(|mt| mt.matches(&resp.headers().get_all(CONTENT_TYPE)))
168
+ {
169
+ debug!(
170
+ target: "middleware::compress",
171
+ "Mime type not supported for compression"
172
+ );
173
+ return resp;
174
+ }
175
+
176
+ // Don't compress streams unless compress streams is enabled.
177
+ if body_size.is_none() && !self.compress_streams {
178
+ debug!(
179
+ target: "middleware::compress",
180
+ "Stream compression disabled"
181
+ );
182
+ return resp;
183
+ }
184
+
185
+ // Don't compress too small bodies
186
+ if body_size.is_some_and(|s| s < self.min_size as u64) {
187
+ debug!(
188
+ target: "middleware::compress",
189
+ "Body size too small for compression"
190
+ );
191
+ return resp;
192
+ }
193
+
194
+ // Don't recompress if we're already compressed in a supported format
195
+ for existing_encoding in resp.headers().get_all(CONTENT_ENCODING) {
196
+ if let Ok(encodings) = existing_encoding.to_str() {
197
+ for encoding in encodings.split(',').map(str::trim) {
198
+ let encoding = encoding.split(';').next().unwrap_or(encoding).trim();
199
+ if self.algorithms.iter().any(|algo| algo.as_str() == encoding) {
200
+ debug!(
201
+ target: "middleware::compress",
202
+ "Body already compressed with supported algorithm"
203
+ );
204
+ return resp;
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ let compression_method = match find_first_supported(
211
+ &context.supported_encoding_set,
212
+ self.algorithms.iter().map(|algo| algo.as_str()),
213
+ ) {
214
+ Some("gzip") => CompressionAlgorithm::Gzip,
215
+ Some("br") => CompressionAlgorithm::Brotli,
216
+ Some("deflate") => CompressionAlgorithm::Deflate,
217
+ Some("zstd") => CompressionAlgorithm::Zstd,
218
+ _ => CompressionAlgorithm::Identity,
219
+ };
220
+
221
+ debug!(
222
+ target: "middleware::compress",
223
+ "Selected compression method: {:?}", compression_method
224
+ );
225
+ if matches!(compression_method, CompressionAlgorithm::Identity) {
226
+ return resp;
227
+ }
228
+
229
+ let (mut parts, body) = resp.into_parts();
230
+
231
+ let new_body = if let Some(_size) = body_size {
232
+ let full_bytes: Bytes = body
233
+ .into_data_stream()
234
+ .try_fold(BytesMut::new(), |mut acc, chunk| async move {
235
+ acc.extend_from_slice(&chunk);
236
+ Ok(acc)
237
+ })
238
+ .await
239
+ .unwrap()
240
+ .freeze();
241
+
242
+ let cursor = std::io::Cursor::new(full_bytes);
243
+ let reader = BufReader::new(cursor);
244
+ let compressed_bytes = match compression_method {
245
+ CompressionAlgorithm::Gzip => {
246
+ let mut encoder =
247
+ GzipEncoder::with_quality(reader, self.level.to_async_compression_level());
248
+ let mut buf = Vec::new();
249
+ encoder.read_to_end(&mut buf).await.unwrap();
250
+ buf
251
+ }
252
+ CompressionAlgorithm::Brotli => {
253
+ let mut encoder = BrotliEncoder::with_quality(
254
+ reader,
255
+ self.level.to_async_compression_level(),
256
+ );
257
+ let mut buf = Vec::new();
258
+ encoder.read_to_end(&mut buf).await.unwrap();
259
+ buf
260
+ }
261
+ CompressionAlgorithm::Deflate => {
262
+ let mut encoder = DeflateEncoder::with_quality(
263
+ reader,
264
+ self.level.to_async_compression_level(),
265
+ );
266
+ let mut buf = Vec::new();
267
+ encoder.read_to_end(&mut buf).await.unwrap();
268
+ buf
269
+ }
270
+ CompressionAlgorithm::Zstd => {
271
+ let mut encoder =
272
+ ZstdEncoder::with_quality(reader, self.level.to_async_compression_level());
273
+ let mut buf = Vec::new();
274
+ encoder.read_to_end(&mut buf).await.unwrap();
275
+ buf
276
+ }
277
+ CompressionAlgorithm::Identity => unreachable!(),
278
+ };
279
+ BoxBody::new(Full::new(Bytes::from(compressed_bytes)))
280
+ } else {
281
+ let stream = body
282
+ .into_data_stream()
283
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
284
+ let async_read_fut = StreamReader::new(stream);
285
+ let reader = BufReader::new(async_read_fut);
286
+ match compression_method {
287
+ CompressionAlgorithm::Gzip => stream_encode(GzipEncoder::with_quality(
288
+ reader,
289
+ self.level.to_async_compression_level(),
290
+ )),
291
+ CompressionAlgorithm::Brotli => stream_encode(BrotliEncoder::with_quality(
292
+ reader,
293
+ self.level.to_async_compression_level(),
294
+ )),
295
+ CompressionAlgorithm::Deflate => stream_encode(DeflateEncoder::with_quality(
296
+ reader,
297
+ self.level.to_async_compression_level(),
298
+ )),
299
+ CompressionAlgorithm::Zstd => stream_encode(ZstdEncoder::with_quality(
300
+ reader,
301
+ self.level.to_async_compression_level(),
302
+ )),
303
+ CompressionAlgorithm::Identity => unreachable!(),
304
+ }
305
+ };
306
+
307
+ update_content_encoding(&mut parts, compression_method.header_value());
308
+ parts.headers.remove(CONTENT_LENGTH);
309
+ debug!(
310
+ target: "middleware::compress",
311
+ "Response compressed"
312
+ );
313
+ Response::from_parts(parts, new_body)
314
+ }
315
+ }
316
+ impl FromValue for Compression {}