itsi-server 0.2.2 → 0.2.4

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +28 -29
  3. data/ext/itsi_scheduler/Cargo.toml +1 -1
  4. data/ext/itsi_server/Cargo.toml +1 -1
  5. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
  6. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +28 -11
  7. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +1 -1
  8. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -2
  9. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +14 -2
  10. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +86 -41
  11. data/ext/itsi_server/src/services/itsi_http_service.rs +46 -35
  12. data/ext/itsi_server/src/services/static_file_server.rs +31 -3
  13. data/lib/itsi/http_request.rb +31 -34
  14. data/lib/itsi/http_response.rb +10 -8
  15. data/lib/itsi/passfile.rb +6 -6
  16. data/lib/itsi/server/config/config_helpers.rb +33 -33
  17. data/lib/itsi/server/config/dsl.rb +16 -21
  18. data/lib/itsi/server/config/known_paths.rb +11 -7
  19. data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +0 -4
  20. data/lib/itsi/server/config/middleware/error_response.md +13 -0
  21. data/lib/itsi/server/config/middleware/location.rb +25 -21
  22. data/lib/itsi/server/config/middleware/proxy.rb +15 -14
  23. data/lib/itsi/server/config/middleware/rackup_file.rb +7 -10
  24. data/lib/itsi/server/config/middleware/static_assets.md +40 -0
  25. data/lib/itsi/server/config/middleware/static_assets.rb +8 -4
  26. data/lib/itsi/server/config/middleware/string_rewrite.md +14 -0
  27. data/lib/itsi/server/config/option.rb +0 -1
  28. data/lib/itsi/server/config/options/include.rb +1 -1
  29. data/lib/itsi/server/config/options/nodelay.md +2 -2
  30. data/lib/itsi/server/config/options/reuse_address.md +1 -1
  31. data/lib/itsi/server/config/typed_struct.rb +32 -35
  32. data/lib/itsi/server/config.rb +107 -92
  33. data/lib/itsi/server/default_app/default_app.rb +1 -1
  34. data/lib/itsi/server/grpc/grpc_call.rb +4 -5
  35. data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
  36. data/lib/itsi/server/rack/handler/itsi.rb +0 -1
  37. data/lib/itsi/server/rack_interface.rb +1 -2
  38. data/lib/itsi/server/route_tester.rb +26 -24
  39. data/lib/itsi/server/typed_handlers/param_parser.rb +25 -0
  40. data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
  41. data/lib/itsi/server/version.rb +1 -1
  42. data/lib/itsi/server.rb +22 -22
  43. data/lib/itsi/standard_headers.rb +80 -80
  44. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 945215c55c27ed88c72156d88ab5606ec321dbb609406a2fa301d620a2dfdb8d
4
- data.tar.gz: 74682a5850c9c2d1a2c449550bbeedd932e153dc8e753e5224fcc98999d51353
3
+ metadata.gz: '09f3db1a1f234effca0f069532c406269afd5d97fd94f2bd84e0897794111c94'
4
+ data.tar.gz: 9b58b1c27aa1be97ffbcda9f58d7d10773c34ec17ed2f8266f7b4515a1916866
5
5
  SHA512:
6
- metadata.gz: 2239fac3020f7b9887765b41ab42ea9ffabe08b338875608857b96e93f307637bcf872f5d855feb8906a5cbffd057ea2b0d6a3e9148d3ce9d706b531cd41a897
7
- data.tar.gz: 23aab8255a3d5e7bb731c32f65ce340a632363b482dba3f1674c99b4bbb5d22728197523d515205ef18a91b19ea8307fc27144b14d22d186ead5de746d6deb21
6
+ metadata.gz: 4b0adee39683e6aa6d8fde02c7caeb448924090c85db779cd210ddc01a6b1ded6944604e16b17a9e18cf8833a8565c6473d3761c0015ab13af56570f0b769850
7
+ data.tar.gz: 2733bfcbb217c88fb2c49f577eb764952551c2c0f6f0be6435505eb55334276367d3ef854bb3aed054f33c192c4c7b5ee02a2ca90d0abf2249b991b1d9a531fd
data/Cargo.lock CHANGED
@@ -117,9 +117,9 @@ dependencies = [
117
117
 
118
118
  [[package]]
119
119
  name = "anyhow"
120
- version = "1.0.97"
120
+ version = "1.0.98"
121
121
  source = "registry+https://github.com/rust-lang/crates.io-index"
122
- checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
122
+ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
123
123
 
124
124
  [[package]]
125
125
  name = "arc-swap"
@@ -253,9 +253,9 @@ dependencies = [
253
253
 
254
254
  [[package]]
255
255
  name = "aws-lc-sys"
256
- version = "0.28.0"
256
+ version = "0.28.2"
257
257
  source = "registry+https://github.com/rust-lang/crates.io-index"
258
- checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f"
258
+ checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
259
259
  dependencies = [
260
260
  "bindgen",
261
261
  "cc",
@@ -467,9 +467,9 @@ dependencies = [
467
467
 
468
468
  [[package]]
469
469
  name = "brotli-decompressor"
470
- version = "4.0.2"
470
+ version = "4.0.3"
471
471
  source = "registry+https://github.com/rust-lang/crates.io-index"
472
- checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
472
+ checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
473
473
  dependencies = [
474
474
  "alloc-no-stdlib",
475
475
  "alloc-stdlib",
@@ -587,9 +587,9 @@ dependencies = [
587
587
 
588
588
  [[package]]
589
589
  name = "clap"
590
- version = "4.5.36"
590
+ version = "4.5.37"
591
591
  source = "registry+https://github.com/rust-lang/crates.io-index"
592
- checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
592
+ checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
593
593
  dependencies = [
594
594
  "clap_builder",
595
595
  "clap_derive",
@@ -597,9 +597,9 @@ dependencies = [
597
597
 
598
598
  [[package]]
599
599
  name = "clap_builder"
600
- version = "4.5.36"
600
+ version = "4.5.37"
601
601
  source = "registry+https://github.com/rust-lang/crates.io-index"
602
- checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
602
+ checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
603
603
  dependencies = [
604
604
  "anstream",
605
605
  "anstyle",
@@ -764,9 +764,9 @@ dependencies = [
764
764
 
765
765
  [[package]]
766
766
  name = "data-encoding"
767
- version = "2.8.0"
767
+ version = "2.9.0"
768
768
  source = "registry+https://github.com/rust-lang/crates.io-index"
769
- checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
769
+ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
770
770
 
771
771
  [[package]]
772
772
  name = "der-parser"
@@ -1176,9 +1176,9 @@ dependencies = [
1176
1176
 
1177
1177
  [[package]]
1178
1178
  name = "h2"
1179
- version = "0.4.8"
1179
+ version = "0.4.9"
1180
1180
  source = "registry+https://github.com/rust-lang/crates.io-index"
1181
- checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
1181
+ checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
1182
1182
  dependencies = [
1183
1183
  "atomic-waker",
1184
1184
  "bytes",
@@ -1363,7 +1363,7 @@ dependencies = [
1363
1363
  "bytes",
1364
1364
  "futures-channel",
1365
1365
  "futures-util",
1366
- "h2 0.4.8",
1366
+ "h2 0.4.9",
1367
1367
  "http 1.3.1",
1368
1368
  "http-body 1.0.1",
1369
1369
  "httparse",
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1644
1644
 
1645
1645
  [[package]]
1646
1646
  name = "itsi-server"
1647
- version = "0.2.2"
1647
+ version = "0.2.4"
1648
1648
  dependencies = [
1649
1649
  "argon2",
1650
1650
  "async-channel",
@@ -1681,7 +1681,7 @@ dependencies = [
1681
1681
  "parking_lot",
1682
1682
  "percent-encoding",
1683
1683
  "pin-project",
1684
- "rand 0.9.0",
1684
+ "rand 0.9.1",
1685
1685
  "rcgen",
1686
1686
  "redis",
1687
1687
  "regex",
@@ -1713,7 +1713,7 @@ dependencies = [
1713
1713
  "axum-server",
1714
1714
  "base64 0.22.1",
1715
1715
  "chrono",
1716
- "clap 4.5.36",
1716
+ "clap 4.5.37",
1717
1717
  "futures",
1718
1718
  "log",
1719
1719
  "num-bigint",
@@ -1841,9 +1841,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
1841
1841
 
1842
1842
  [[package]]
1843
1843
  name = "libc"
1844
- version = "0.2.171"
1844
+ version = "0.2.172"
1845
1845
  source = "registry+https://github.com/rust-lang/crates.io-index"
1846
- checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
1846
+ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
1847
1847
 
1848
1848
  [[package]]
1849
1849
  name = "libloading"
@@ -1852,7 +1852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1852
1852
  checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
1853
1853
  dependencies = [
1854
1854
  "cfg-if",
1855
- "windows-targets 0.52.6",
1855
+ "windows-targets 0.48.5",
1856
1856
  ]
1857
1857
 
1858
1858
  [[package]]
@@ -2366,9 +2366,9 @@ dependencies = [
2366
2366
 
2367
2367
  [[package]]
2368
2368
  name = "proc-macro2"
2369
- version = "1.0.94"
2369
+ version = "1.0.95"
2370
2370
  source = "registry+https://github.com/rust-lang/crates.io-index"
2371
- checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
2371
+ checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
2372
2372
  dependencies = [
2373
2373
  "unicode-ident",
2374
2374
  ]
@@ -2401,7 +2401,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
2401
2401
  dependencies = [
2402
2402
  "bytes",
2403
2403
  "getrandom 0.3.2",
2404
- "rand 0.9.0",
2404
+ "rand 0.9.1",
2405
2405
  "ring",
2406
2406
  "rustc-hash 2.1.1",
2407
2407
  "rustls",
@@ -2466,13 +2466,12 @@ dependencies = [
2466
2466
 
2467
2467
  [[package]]
2468
2468
  name = "rand"
2469
- version = "0.9.0"
2469
+ version = "0.9.1"
2470
2470
  source = "registry+https://github.com/rust-lang/crates.io-index"
2471
- checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
2471
+ checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
2472
2472
  dependencies = [
2473
2473
  "rand_chacha 0.9.0",
2474
2474
  "rand_core 0.9.3",
2475
- "zerocopy",
2476
2475
  ]
2477
2476
 
2478
2477
  [[package]]
@@ -3062,9 +3061,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
3062
3061
 
3063
3062
  [[package]]
3064
3063
  name = "signal-hook-registry"
3065
- version = "1.4.2"
3064
+ version = "1.4.5"
3066
3065
  source = "registry+https://github.com/rust-lang/crates.io-index"
3067
- checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
3066
+ checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
3068
3067
  dependencies = [
3069
3068
  "libc",
3070
3069
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-scheduler"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-server"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -14,7 +14,7 @@ use magnus::{
14
14
  block::Proc,
15
15
  error::Result,
16
16
  value::{LazyId, ReprValue},
17
- RArray, RHash, Ruby, Symbol, Value,
17
+ RArray, RHash, Ruby, Symbol, TryConvert, Value,
18
18
  };
19
19
  use nix::{
20
20
  fcntl::{fcntl, FcntlArg, FdFlag},
@@ -124,9 +124,17 @@ impl ServerParams {
124
124
  debug!("Loading Itsi Scheduler");
125
125
  ruby.require("itsi/scheduler")?;
126
126
  }
127
- let routes_raw = self
127
+ let result_pair = self
128
128
  .middleware_loader
129
- .call::<_, Option<Value>>(())
129
+ .call::<(), RArray>(())
130
+ .inspect_err(|e| {
131
+ eprintln!("Error loading middleware: {:?}", e);
132
+ if let Some(err_value) = e.value() {
133
+ print_rb_backtrace(err_value);
134
+ }
135
+ })?;
136
+ let routes_raw = result_pair
137
+ .entry::<Option<Value>>(0)
130
138
  .inspect_err(|e| {
131
139
  eprintln!("Error loading middleware: {:?}", e);
132
140
  if let Some(err_value) = e.value() {
@@ -134,6 +142,21 @@ impl ServerParams {
134
142
  }
135
143
  })?
136
144
  .map(|mw| mw.into());
145
+ let error_lines = result_pair.entry::<Option<RArray>>(1).inspect_err(|e| {
146
+ eprintln!("Error loading middleware: {:?}", e);
147
+ if let Some(err_value) = e.value() {
148
+ print_rb_backtrace(err_value);
149
+ }
150
+ })?;
151
+ if error_lines.is_some_and(|r| !r.is_empty()) {
152
+ let errors: Vec<String> =
153
+ Vec::<String>::try_convert(error_lines.unwrap().as_value())?;
154
+ ItsiServerConfig::print_config_errors(errors);
155
+ return Err(magnus::Error::new(
156
+ magnus::exception::runtime_error(),
157
+ "Failed to set middleware",
158
+ ));
159
+ }
137
160
  let middleware = MiddlewareSet::new(routes_raw)?;
138
161
  self.middleware.set(middleware).map_err(|_| {
139
162
  magnus::Error::new(
@@ -1,5 +1,6 @@
1
1
  use crate::{
2
- server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
2
+ server::http_message_types::{HttpRequest, HttpResponse},
3
+ services::itsi_http_service::HttpRequestContext,
3
4
  };
4
5
 
5
6
  use super::{
@@ -13,6 +14,7 @@ use async_compression::{
13
14
  };
14
15
  use async_trait::async_trait;
15
16
  use bytes::{Bytes, BytesMut};
17
+ use either::Either;
16
18
  use futures::TryStreamExt;
17
19
  use http::{
18
20
  header::{GetAll, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
@@ -20,6 +22,7 @@ use http::{
20
22
  };
21
23
  use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
22
24
  use hyper::body::{Body, Frame};
25
+ use magnus::error::Result;
23
26
  use serde::{Deserialize, Serialize};
24
27
  use std::convert::Infallible;
25
28
  use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
@@ -151,6 +154,15 @@ fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: Head
151
154
 
152
155
  #[async_trait]
153
156
  impl MiddlewareLayer for Compression {
157
+ async fn before(
158
+ &self,
159
+ req: HttpRequest,
160
+ context: &mut HttpRequestContext,
161
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
162
+ context.set_supported_encoding_set(&req);
163
+ Ok(Either::Left(req))
164
+ }
165
+
154
166
  /// We'll apply compression on the response, where appropriate.
155
167
  /// This is if:
156
168
  /// * The response body is larger than the minimum size.
@@ -207,16 +219,21 @@ impl MiddlewareLayer for Compression {
207
219
  }
208
220
  }
209
221
 
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
- };
222
+ let compression_method =
223
+ if let Some(supported_encoding_set) = context.supported_encoding_set() {
224
+ match find_first_supported(
225
+ supported_encoding_set,
226
+ self.algorithms.iter().map(|algo| algo.as_str()),
227
+ ) {
228
+ Some("gzip") => CompressionAlgorithm::Gzip,
229
+ Some("br") => CompressionAlgorithm::Brotli,
230
+ Some("deflate") => CompressionAlgorithm::Deflate,
231
+ Some("zstd") => CompressionAlgorithm::Zstd,
232
+ _ => CompressionAlgorithm::Identity,
233
+ }
234
+ } else {
235
+ CompressionAlgorithm::Identity
236
+ };
220
237
 
221
238
  debug!(
222
239
  target: "middleware::compress",
@@ -62,7 +62,7 @@ impl MiddlewareLayer for LogRequests {
62
62
  req: HttpRequest,
63
63
  context: &mut HttpRequestContext,
64
64
  ) -> Result<Either<HttpRequest, HttpResponse>> {
65
- context.track_start_time();
65
+ context.init_logging_params();
66
66
  if let Some(LogConfig { level, format }) = self.before.as_ref() {
67
67
  level.log(format.rewrite_request(&req, context));
68
68
  }
@@ -33,7 +33,7 @@ use reqwest::{
33
33
  Body, Client, Url,
34
34
  };
35
35
  use serde::Deserialize;
36
- use tracing::{debug, info};
36
+ use tracing::debug;
37
37
 
38
38
  #[derive(Debug, Clone, Deserialize)]
39
39
  pub struct Proxy {
@@ -324,7 +324,6 @@ impl MiddlewareLayer for Proxy {
324
324
  .unwrap_or("")
325
325
  });
326
326
 
327
- info!("Extracted host str is {}", host_str);
328
327
  let req_info = RequestInfo {
329
328
  method: req.method().clone(),
330
329
  headers: req_headers.clone(),
@@ -12,10 +12,11 @@ use async_trait::async_trait;
12
12
  use either::Either;
13
13
  use http::{
14
14
  header::{IF_MODIFIED_SINCE, RANGE},
15
- HeaderMap, Method,
15
+ HeaderMap, HeaderValue, Method,
16
16
  };
17
17
  use itsi_error::ItsiError;
18
18
  use magnus::error::Result;
19
+ use moka::sync::Cache;
19
20
  use regex::Regex;
20
21
  use serde::Deserialize;
21
22
  use std::{collections::HashMap, path::PathBuf, sync::OnceLock, time::Duration};
@@ -75,6 +76,10 @@ impl MiddlewareLayer for StaticAssets {
75
76
  recheck_interval: Duration::from_secs(self.file_check_interval),
76
77
  serve_hidden_files: self.serve_hidden_files,
77
78
  allowed_extensions: self.allowed_extensions.clone(),
79
+ miss_cache: Cache::builder()
80
+ .max_capacity(self.max_files_in_memory)
81
+ .time_to_live(Duration::from_secs(self.file_check_interval))
82
+ .build(),
78
83
  })?)
79
84
  .map_err(ItsiError::new)?;
80
85
  Ok(())
@@ -90,6 +95,8 @@ impl MiddlewareLayer for StaticAssets {
90
95
  debug!(target: "middleware::static_assets", "Refusing to handle non-GET/HEAD request");
91
96
  return Ok(Either::Left(req));
92
97
  }
98
+
99
+ context.set_supported_encoding_set(&req);
93
100
  let abs_path = req.uri().path();
94
101
  let rel_path = if !self.relative_path {
95
102
  abs_path.trim_start_matches("/")
@@ -123,6 +130,10 @@ impl MiddlewareLayer for StaticAssets {
123
130
 
124
131
  // Let the file server handle everything
125
132
  let file_server = self.file_server.get().unwrap();
133
+ let encodings: &[HeaderValue] = context
134
+ .supported_encoding_set()
135
+ .map(Vec::as_slice)
136
+ .unwrap_or(&[] as &[HeaderValue]);
126
137
  let response = file_server
127
138
  .serve(
128
139
  &req,
@@ -131,9 +142,10 @@ impl MiddlewareLayer for StaticAssets {
131
142
  serve_range,
132
143
  if_modified_since,
133
144
  is_head_request,
134
- &context.supported_encoding_set,
145
+ encodings,
135
146
  )
136
147
  .await;
148
+
137
149
  if response.is_none() {
138
150
  Ok(Either::Left(req))
139
151
  } else {
@@ -50,6 +50,35 @@ pub fn parse_template(template: &str) -> Vec<Segment> {
50
50
  }
51
51
 
52
52
  impl StringRewrite {
53
+ /// Apply a single modifier of the form `op:arg` (or for replace `op:from,to`)
54
+ #[inline]
55
+ fn apply_modifier(s: &mut String, mod_str: &str) {
56
+ if let Some((op, arg)) = mod_str.split_once(':') {
57
+ match op {
58
+ "strip_prefix" => {
59
+ if s.starts_with(arg) {
60
+ let _ = s.drain(..arg.len());
61
+ }
62
+ }
63
+ "strip_suffix" => {
64
+ if s.ends_with(arg) {
65
+ let len = s.len();
66
+ let start = len.saturating_sub(arg.len());
67
+ let _ = s.drain(start..);
68
+ }
69
+ }
70
+ "replace" => {
71
+ if let Some((from, to)) = arg.split_once(',') {
72
+ if s.contains(from) {
73
+ *s = s.replace(from, to);
74
+ }
75
+ }
76
+ }
77
+ _ => {}
78
+ }
79
+ }
80
+ }
81
+
53
82
  pub fn rewrite_request(&self, req: &HttpRequest, context: &HttpRequestContext) -> String {
54
83
  let segments = self
55
84
  .segments
@@ -63,9 +92,17 @@ impl StringRewrite {
63
92
 
64
93
  for segment in segments {
65
94
  match segment {
66
- Segment::Literal(text) => result.push_str(text),
67
- Segment::Placeholder(placeholder) => {
68
- let replacement = match placeholder.as_str() {
95
+ Segment::Literal(text) => {
96
+ result.push_str(text);
97
+ }
98
+ Segment::Placeholder(raw) => {
99
+ // split into key and optional modifier
100
+ let mut parts = raw.split('|');
101
+ let key = parts.next().unwrap();
102
+ let modifiers = parts; // zero o
103
+
104
+ // 1) lookup the raw replacement
105
+ let mut replacement = match key {
69
106
  "request_id" => context.short_request_id(),
70
107
  "request_id_full" => context.request_id(),
71
108
  "method" => req.method().as_str().to_string(),
@@ -76,13 +113,13 @@ impl StringRewrite {
76
113
  .uri()
77
114
  .path_and_query()
78
115
  .map(|pq| pq.to_string())
79
- .unwrap_or("".to_string()),
116
+ .unwrap_or_default(),
80
117
  "query" => {
81
- let query = req.uri().query().unwrap_or("").to_string();
82
- if query.is_empty() {
83
- query
118
+ let q = req.uri().query().unwrap_or("");
119
+ if q.is_empty() {
120
+ "".to_string()
84
121
  } else {
85
- format!("?{}", query)
122
+ format!("?{}", q)
86
123
  }
87
124
  }
88
125
  "port" => req
@@ -91,31 +128,34 @@ impl StringRewrite {
91
128
  .map(|p| p.to_string())
92
129
  .unwrap_or_else(|| "80".to_string()),
93
130
  "start_time" => {
94
- if let Some(start_time) = context.start_time() {
95
- start_time.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
131
+ if let Some(ts) = context.start_time() {
132
+ ts.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
96
133
  } else {
97
134
  "N/A".to_string()
98
135
  }
99
136
  }
100
137
  other => {
101
- if let Some(header_val) = req.headers().get(other) {
102
- if let Ok(s) = header_val.to_str() {
103
- s.to_string()
104
- } else {
105
- "".to_string()
106
- }
107
- } else if let Some(caps) = &captures {
108
- if let Some(m) = caps.name(other) {
109
- m.as_str().to_string()
110
- } else {
111
- // Fallback: leave the placeholder as is.
112
- format!("{{{}}}", other)
113
- }
114
- } else {
138
+ // headers first
139
+ if let Some(hv) = req.headers().get(other) {
140
+ hv.to_str().unwrap_or("").to_string()
141
+ }
142
+ // then any regex‐capture
143
+ else if let Some(caps) = &captures {
144
+ caps.name(other)
145
+ .map(|m| m.as_str().to_string())
146
+ .unwrap_or_else(|| format!("{{{}}}", other))
147
+ }
148
+ // fallback: leave placeholder intact
149
+ else {
115
150
  format!("{{{}}}", other)
116
151
  }
117
152
  }
118
153
  };
154
+
155
+ for m in modifiers {
156
+ Self::apply_modifier(&mut replacement, m);
157
+ }
158
+
119
159
  result.push_str(&replacement);
120
160
  }
121
161
  }
@@ -132,37 +172,42 @@ impl StringRewrite {
132
172
  let mut result = String::with_capacity(self.template_string.len());
133
173
  for segment in segments {
134
174
  match segment {
135
- Segment::Literal(text) => result.push_str(text),
136
- Segment::Placeholder(placeholder) => {
137
- let replacement = match placeholder.as_str() {
175
+ Segment::Literal(text) => {
176
+ result.push_str(text);
177
+ }
178
+ Segment::Placeholder(raw) => {
179
+ let mut parts = raw.split('|');
180
+ let key = parts.next().unwrap();
181
+ let modifiers = parts; // zero o
182
+
183
+ let mut replacement = match key {
138
184
  "request_id" => context.short_request_id(),
139
185
  "request_id_full" => context.request_id(),
140
186
  "status" => resp.status().as_str().to_string(),
141
187
  "addr" => context.addr.to_owned(),
142
188
  "response_time" => {
143
- if let Some(response_time) = context.get_response_time() {
144
- if let Some(microseconds) = response_time.num_microseconds() {
145
- format!("{:.3}ms", microseconds as f64 / 1000.0)
146
- } else {
147
- format!("{}ms", response_time.num_milliseconds())
148
- }
189
+ let dur = context.get_response_time();
190
+ let micros = dur.as_micros();
191
+ if micros < 1_000 {
192
+ format!("{}µs", micros)
149
193
  } else {
150
- "-".to_string()
194
+ let ms = dur.as_secs_f64() * 1_000.0;
195
+ format!("{:.3}ms", ms)
151
196
  }
152
197
  }
153
198
  other => {
154
- // Try pulling from response headers first
155
- if let Some(val) = resp.headers().get(other) {
156
- if let Ok(s) = val.to_str() {
157
- s.to_string()
158
- } else {
159
- "".to_string()
160
- }
199
+ if let Some(hv) = resp.headers().get(other) {
200
+ hv.to_str().unwrap_or("").to_string()
161
201
  } else {
162
202
  format!("{{{}}}", other)
163
203
  }
164
204
  }
165
205
  };
206
+
207
+ for m in modifiers {
208
+ Self::apply_modifier(&mut replacement, m);
209
+ }
210
+
166
211
  result.push_str(&replacement);
167
212
  }
168
213
  }