1use std::sync::Arc;
5use std::time::Duration;
6
7use rainier_http::{IntoResponse, Method, Request, Response, StatusCode};
8use serde_json::Value;
9
10use crate::pipeline::{Middleware, Next};
11use crate::rate_limit::{MemoryRateLimitStore, RateLimitStore};
12
13#[derive(Debug, Clone)]
20pub struct TrimStrings {
21 except: Vec<String>,
22}
23
24impl Default for TrimStrings {
25 fn default() -> Self {
26 Self { except: vec!["password".into(), "password_confirmation".into()] }
27 }
28}
29
30impl TrimStrings {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn except(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
38 self.except = keys.into_iter().map(Into::into).collect();
39 self
40 }
41
42 fn apply(&self, value: Value) -> Value {
43 match value {
44 Value::String(s) => Value::String(s.trim().to_string()),
45 Value::Array(items) => {
46 Value::Array(items.into_iter().map(|item| self.apply(item)).collect())
47 }
48 Value::Object(map) => Value::Object(
49 map.into_iter()
50 .map(|(key, item)| {
51 let trimmed =
52 if self.except.contains(&key) { item } else { self.apply(item) };
53 (key, trimmed)
54 })
55 .collect(),
56 ),
57 other => other,
58 }
59 }
60}
61
62#[async_trait::async_trait]
63impl Middleware for TrimStrings {
64 async fn handle(&self, mut request: Request, next: Next) -> Response {
65 request.transform_input(|input| self.apply(input));
66 next.run(request).await
67 }
68
69 fn name(&self) -> &'static str {
70 "TrimStrings"
71 }
72}
73
74#[derive(Debug, Clone, Default)]
81pub struct ConvertEmptyStringsToNull;
82
83impl ConvertEmptyStringsToNull {
84 fn apply(value: Value) -> Value {
85 match value {
86 Value::String(s) if s.is_empty() => Value::Null,
87 Value::Array(items) => Value::Array(items.into_iter().map(Self::apply).collect()),
88 Value::Object(map) => {
89 Value::Object(map.into_iter().map(|(k, v)| (k, Self::apply(v))).collect())
90 }
91 other => other,
92 }
93 }
94}
95
96#[async_trait::async_trait]
97impl Middleware for ConvertEmptyStringsToNull {
98 async fn handle(&self, mut request: Request, next: Next) -> Response {
99 request.transform_input(Self::apply);
100 next.run(request).await
101 }
102
103 fn name(&self) -> &'static str {
104 "ConvertEmptyStringsToNull"
105 }
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct AddHeaders {
111 headers: Vec<(String, String)>,
112}
113
114impl AddHeaders {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn security_defaults() -> Self {
127 Self::new()
128 .with("x-content-type-options", "nosniff")
129 .with("x-frame-options", "SAMEORIGIN")
130 .with("referrer-policy", "strict-origin-when-cross-origin")
131 }
132
133 pub fn with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
135 self.headers.push((name.into(), value.into()));
136 self
137 }
138}
139
140#[async_trait::async_trait]
141impl Middleware for AddHeaders {
142 async fn handle(&self, request: Request, next: Next) -> Response {
143 let mut response = next.run(request).await;
144 for (name, value) in &self.headers {
145 response = response.with_header(name, value);
146 }
147 response
148 }
149
150 fn name(&self) -> &'static str {
151 "AddHeaders"
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum AllowedOrigins {
158 Any,
160 List(Vec<String>),
162}
163
164#[derive(Debug, Clone)]
170pub struct HandleCors {
171 origins: AllowedOrigins,
172 methods: Vec<String>,
173 headers: Vec<String>,
174 exposed: Vec<String>,
175 credentials: bool,
176 max_age: u32,
177}
178
179impl Default for HandleCors {
180 fn default() -> Self {
181 Self {
182 origins: AllowedOrigins::Any,
183 methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
184 .iter()
185 .map(|m| m.to_string())
186 .collect(),
187 headers: ["content-type", "authorization", "x-requested-with"]
188 .iter()
189 .map(|h| h.to_string())
190 .collect(),
191 exposed: Vec::new(),
192 credentials: false,
193 max_age: 86_400,
194 }
195 }
196}
197
198impl HandleCors {
199 pub fn any_origin() -> Self {
201 Self::default()
202 }
203
204 pub fn allow_origins(mut self, origins: impl IntoIterator<Item = impl Into<String>>) -> Self {
206 self.origins = AllowedOrigins::List(origins.into_iter().map(Into::into).collect());
207 self
208 }
209
210 pub fn allow_methods(mut self, methods: impl IntoIterator<Item = impl Into<String>>) -> Self {
212 self.methods = methods.into_iter().map(Into::into).collect();
213 self
214 }
215
216 pub fn allow_headers(mut self, headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
218 self.headers = headers.into_iter().map(Into::into).collect();
219 self
220 }
221
222 pub fn expose_headers(mut self, headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
224 self.exposed = headers.into_iter().map(Into::into).collect();
225 self
226 }
227
228 pub fn allow_credentials(mut self, allow: bool) -> Self {
236 self.credentials = allow;
237 self
238 }
239
240 pub fn max_age(mut self, seconds: u32) -> Self {
242 self.max_age = seconds;
243 self
244 }
245
246 pub fn allowed_origin_for(&self, origin: Option<&str>) -> Option<String> {
249 match &self.origins {
250 AllowedOrigins::Any if self.credentials => origin.map(str::to_string),
251 AllowedOrigins::Any => Some("*".to_string()),
252 AllowedOrigins::List(allowed) => {
253 let origin = origin?;
254 allowed.iter().any(|a| a == origin).then(|| origin.to_string())
255 }
256 }
257 }
258
259 fn decorate(&self, response: Response, allowed_origin: Option<String>) -> Response {
260 let Some(origin) = allowed_origin else {
261 return response;
262 };
263
264 let mut response = response.with_header("access-control-allow-origin", &origin);
265 if origin != "*" {
266 response = response.with_added_header("vary", "Origin");
268 }
269 if self.credentials {
270 response = response.with_header("access-control-allow-credentials", "true");
271 }
272 if !self.exposed.is_empty() {
273 response =
274 response.with_header("access-control-expose-headers", &self.exposed.join(", "));
275 }
276 response
277 }
278}
279
280#[async_trait::async_trait]
281impl Middleware for HandleCors {
282 async fn handle(&self, request: Request, next: Next) -> Response {
283 let origin = request.header("origin").map(str::to_string);
284 let allowed = self.allowed_origin_for(origin.as_deref());
285
286 if request.method() == Method::OPTIONS
289 && request.header("access-control-request-method").is_some()
290 {
291 let response = Response::new(StatusCode::NO_CONTENT)
292 .with_header("access-control-allow-methods", &self.methods.join(", "))
293 .with_header("access-control-allow-headers", &self.headers.join(", "))
294 .with_header("access-control-max-age", &self.max_age.to_string());
295 return self.decorate(response, allowed);
296 }
297
298 let response = next.run(request).await;
299 self.decorate(response, allowed)
300 }
301
302 fn name(&self) -> &'static str {
303 "HandleCors"
304 }
305}
306
307#[derive(Clone)]
338pub struct ThrottleRequests {
339 max_attempts: u32,
340 window: Duration,
341 name: Option<String>,
344 key_by: Option<KeyFn>,
347 store: Arc<dyn RateLimitStore>,
348}
349
350type KeyFn = Arc<dyn Fn(&Request) -> Option<String> + Send + Sync>;
356
357impl ThrottleRequests {
358 pub fn new(max_attempts: u32, window: Duration) -> Self {
360 Self {
361 max_attempts,
362 window,
363 name: None,
364 key_by: None,
365 store: Arc::new(MemoryRateLimitStore::new()),
366 }
367 }
368
369 pub fn per_minute(max_attempts: u32) -> Self {
371 Self::new(max_attempts, Duration::from_secs(60))
372 }
373
374 pub fn per_hour(max_attempts: u32) -> Self {
376 Self::new(max_attempts, Duration::from_secs(3600))
377 }
378
379 pub fn per_day(max_attempts: u32) -> Self {
381 Self::new(max_attempts, Duration::from_secs(86_400))
382 }
383
384 #[must_use = "this returns a configured throttle rather than configuring in place"]
389 pub fn stored_in(mut self, store: Arc<dyn RateLimitStore>) -> Self {
390 self.store = store;
391 self
392 }
393
394 #[must_use = "this returns a configured throttle rather than configuring in place"]
403 pub fn named(mut self, name: impl Into<String>) -> Self {
404 self.name = Some(name.into());
405 self
406 }
407
408 #[must_use = "this returns a configured throttle rather than configuring in place"]
417 pub fn keyed_by<F>(mut self, key: F) -> Self
418 where
419 F: Fn(&Request) -> Option<String> + Send + Sync + 'static,
420 {
421 self.key_by = Some(Arc::new(key));
422 self
423 }
424
425 pub fn is_shared(&self) -> bool {
429 self.store.is_shared()
430 }
431
432 pub fn max_attempts(&self) -> u32 {
434 self.max_attempts
435 }
436
437 pub fn limiter_name(&self) -> Option<&str> {
439 self.name.as_deref()
440 }
441
442 fn key(&self, request: &Request) -> Option<String> {
444 let key = match &self.key_by {
445 Some(key_by) => key_by(request)?,
446 None => match request.bearer_token() {
447 Some(token) => format!("token:{token}"),
448 None => match request.ip() {
449 Some(ip) => format!("ip:{ip}"),
450 None => "anonymous".to_string(),
451 },
452 },
453 };
454
455 Some(match &self.name {
456 Some(name) => format!("throttle:{name}:{key}"),
457 None => format!("throttle:{key}"),
458 })
459 }
460}
461
462impl std::fmt::Debug for ThrottleRequests {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct("ThrottleRequests")
465 .field("max_attempts", &self.max_attempts)
466 .field("window", &self.window)
467 .field("name", &self.name)
468 .field("store", &self.store.name())
469 .field("keyed", &if self.key_by.is_some() { "custom" } else { "token-or-ip" })
470 .finish()
471 }
472}
473
474#[async_trait::async_trait]
475impl Middleware for ThrottleRequests {
476 async fn handle(&self, request: Request, next: Next) -> Response {
477 let Some(key) = self.key(&request) else {
478 return next.run(request).await;
482 };
483
484 let hit = match self.store.hit(&key, self.window).await {
485 Ok(hit) => hit,
486 Err(e) => {
491 tracing::error!(
492 error = %e.message(),
493 limiter = self.name.as_deref().unwrap_or("unnamed"),
494 "the rate limit store is unreachable; allowing the request"
495 );
496 return next.run(request).await;
497 }
498 };
499
500 let limit = self.max_attempts.to_string();
501
502 if hit.count > self.max_attempts {
503 let retry_after = hit.resets_in.as_secs() + 1;
504
505 return rainier_support::Error::too_many_requests("Too many requests.")
506 .into_response()
507 .with_header("retry-after", &retry_after.to_string())
508 .with_header("x-ratelimit-limit", &limit)
509 .with_header("x-ratelimit-remaining", "0");
510 }
511
512 let remaining = self.max_attempts - hit.count;
513
514 next.run(request)
515 .await
516 .with_header("x-ratelimit-limit", &limit)
517 .with_header("x-ratelimit-remaining", &remaining.to_string())
518 }
519
520 fn name(&self) -> &'static str {
521 "ThrottleRequests"
522 }
523}
524pub type SharedThrottle = Arc<ThrottleRequests>;
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::pipeline::Pipeline;
532 use rainier_http::ClientIp;
533 use serde_json::json;
534
535 async fn echo_input(request: Request) -> Response {
536 Response::json(&request.all())
537 }
538
539 async fn body_json(response: Response) -> Value {
540 let bytes = response.into_http().into_body().collect().await.unwrap();
541 serde_json::from_slice(&bytes).unwrap()
542 }
543
544 #[tokio::test]
545 async fn trim_strings_trims_recursively() {
546 let request = Request::builder()
547 .method(Method::POST)
548 .json(&json!({ "name": " ada ", "tags": [" a ", "b"], "nested": { "x": " y " } }))
549 .build();
550
551 let response =
552 Pipeline::new().through(TrimStrings::new()).then(echo_input).run(request).await;
553
554 assert_eq!(
555 body_json(response).await,
556 json!({ "name": "ada", "tags": ["a", "b"], "nested": { "x": "y" } })
557 );
558 }
559
560 #[tokio::test]
561 async fn trim_strings_leaves_passwords_alone() {
562 let request = Request::builder()
563 .method(Method::POST)
564 .json(&json!({ "password": " spaces matter ", "email": " a@b.c " }))
565 .build();
566
567 let response =
568 Pipeline::new().through(TrimStrings::new()).then(echo_input).run(request).await;
569
570 assert_eq!(
571 body_json(response).await,
572 json!({ "password": " spaces matter ", "email": "a@b.c" })
573 );
574 }
575
576 #[tokio::test]
577 async fn empty_strings_become_null() {
578 let request = Request::builder()
579 .method(Method::POST)
580 .json(&json!({ "nickname": "", "name": "ada", "list": [""] }))
581 .build();
582
583 let response =
584 Pipeline::new().through(ConvertEmptyStringsToNull).then(echo_input).run(request).await;
585
586 assert_eq!(
587 body_json(response).await,
588 json!({ "nickname": null, "name": "ada", "list": [null] })
589 );
590 }
591
592 #[tokio::test]
593 async fn trimming_then_nulling_turns_blank_into_null() {
594 let request =
595 Request::builder().method(Method::POST).json(&json!({ "nickname": " " })).build();
596
597 let response = Pipeline::new()
598 .through(TrimStrings::new())
599 .through(ConvertEmptyStringsToNull)
600 .then(echo_input)
601 .run(request)
602 .await;
603
604 assert_eq!(body_json(response).await, json!({ "nickname": null }));
605 }
606
607 #[tokio::test]
608 async fn add_headers_decorates_the_response() {
609 let response = Pipeline::new()
610 .through(AddHeaders::security_defaults())
611 .then(|_: Request| async { Response::text("ok") })
612 .run(Request::builder().build())
613 .await;
614
615 assert_eq!(response.header("x-content-type-options"), Some("nosniff"));
616 assert_eq!(response.header("x-frame-options"), Some("SAMEORIGIN"));
617 assert!(response.header("strict-transport-security").is_none());
618 }
619
620 #[tokio::test]
621 async fn cors_answers_a_preflight_without_reaching_the_route() {
622 let request = Request::builder()
623 .method(Method::OPTIONS)
624 .header("origin", "https://app.example")
625 .header("access-control-request-method", "POST")
626 .build();
627
628 let response = Pipeline::new()
629 .through(HandleCors::any_origin())
630 .then(|_: Request| async { panic!("a preflight must not reach the route") })
631 .run(request)
632 .await;
633
634 assert_eq!(response.status(), StatusCode::NO_CONTENT);
635 assert_eq!(response.header("access-control-allow-origin"), Some("*"));
636 assert!(response.header("access-control-allow-methods").unwrap().contains("POST"));
637 assert_eq!(response.header("access-control-max-age"), Some("86400"));
638 }
639
640 #[tokio::test]
641 async fn cors_decorates_an_ordinary_response() {
642 let request = Request::builder().header("origin", "https://app.example").build();
643 let response = Pipeline::new()
644 .through(HandleCors::any_origin())
645 .then(|_: Request| async { Response::text("ok") })
646 .run(request)
647 .await;
648
649 assert_eq!(response.status(), StatusCode::OK);
650 assert_eq!(response.header("access-control-allow-origin"), Some("*"));
651 }
652
653 #[test]
654 fn cors_origin_matching() {
655 let any = HandleCors::any_origin();
656 assert_eq!(any.allowed_origin_for(Some("https://a.example")).as_deref(), Some("*"));
657 assert_eq!(any.allowed_origin_for(None).as_deref(), Some("*"));
658
659 let listed = HandleCors::any_origin().allow_origins(["https://a.example"]);
660 assert_eq!(
661 listed.allowed_origin_for(Some("https://a.example")).as_deref(),
662 Some("https://a.example")
663 );
664 assert_eq!(listed.allowed_origin_for(Some("https://evil.example")), None);
665 assert_eq!(listed.allowed_origin_for(None), None);
666 }
667
668 #[test]
669 fn credentials_never_pair_with_a_wildcard_origin() {
670 let policy = HandleCors::any_origin().allow_credentials(true);
671 assert_eq!(
673 policy.allowed_origin_for(Some("https://a.example")).as_deref(),
674 Some("https://a.example")
675 );
676 }
677
678 #[tokio::test]
679 async fn a_restricted_origin_response_varies_on_origin() {
680 let request = Request::builder().header("origin", "https://a.example").build();
681 let response = Pipeline::new()
682 .through(HandleCors::any_origin().allow_origins(["https://a.example"]))
683 .then(|_: Request| async { Response::text("ok") })
684 .run(request)
685 .await;
686
687 assert_eq!(response.header("vary"), Some("Origin"));
688 }
689
690 fn from_ip(n: u8) -> Request {
691 Request::builder().build().with_extension(ClientIp(std::net::IpAddr::from([127, 0, 0, n])))
692 }
693
694 #[tokio::test]
695 async fn throttling_allows_up_to_the_limit_then_rejects() {
696 let throttle: SharedThrottle = Arc::new(ThrottleRequests::per_minute(2));
697
698 for expected_remaining in ["1", "0"] {
699 let response = Pipeline::new()
700 .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
701 .then(|_: Request| async { Response::text("ok") })
702 .run(from_ip(1))
703 .await;
704 assert_eq!(response.status(), StatusCode::OK);
705 assert_eq!(response.header("x-ratelimit-remaining"), Some(expected_remaining));
706 }
707
708 let blocked = Pipeline::new()
709 .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
710 .then(|_: Request| async { panic!("must not reach the route once throttled") })
711 .run(from_ip(1))
712 .await;
713
714 assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
715 assert!(blocked.header("retry-after").is_some());
716 }
717
718 #[tokio::test]
719 async fn throttling_counts_each_client_separately() {
720 let throttle: SharedThrottle = Arc::new(ThrottleRequests::per_minute(1));
721
722 for ip in [1, 2] {
723 let response = Pipeline::new()
724 .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
725 .then(|_: Request| async { Response::text("ok") })
726 .run(from_ip(ip))
727 .await;
728 assert_eq!(response.status(), StatusCode::OK, "ip {ip} should have its own allowance");
729 }
730 }
731
732 #[tokio::test]
733 async fn throttling_keys_authenticated_requests_by_token() {
734 let throttle = ThrottleRequests::per_minute(1);
735
736 let with_token = |token: &str| {
737 Request::builder().header("authorization", &format!("Bearer {token}")).build()
738 };
739
740 assert_eq!(throttle.key(&with_token("a")).as_deref(), Some("throttle:token:a"));
741 assert_ne!(throttle.key(&with_token("a")), throttle.key(&with_token("b")));
742 }
743
744 #[tokio::test]
745 async fn a_name_namespaces_the_key() {
746 let plain = ThrottleRequests::per_minute(1);
749 let named = ThrottleRequests::per_minute(1).named("login");
750
751 let request = Request::builder().header("authorization", "Bearer a").build();
752
753 assert_eq!(named.key(&request).as_deref(), Some("throttle:login:token:a"));
754 assert_ne!(plain.key(&request), named.key(&request));
755 }
756
757 #[tokio::test]
758 async fn a_custom_key_counts_what_was_submitted() {
759 let throttle =
760 ThrottleRequests::per_minute(1).named("login").keyed_by(|r| r.input("email"));
761
762 let request = Request::builder()
763 .method(Method::POST)
764 .uri("/login")
765 .json(&json!({ "email": "ada@example.com" }))
766 .build();
767
768 assert_eq!(throttle.key(&request).as_deref(), Some("throttle:login:ada@example.com"));
769 }
770
771 #[tokio::test]
772 async fn a_request_with_nothing_to_key_on_is_not_counted() {
773 let throttle = ThrottleRequests::per_minute(1).keyed_by(|r| r.input("email"));
776 let request = Request::builder().method(Method::POST).uri("/login").build();
777
778 assert_eq!(throttle.key(&request), None);
779
780 for _ in 0..5 {
782 let response = Pipeline::new()
783 .through(ThrottleRequests::per_minute(1).keyed_by(|r| r.input("email")))
784 .then(|_| async { Response::ok("through") })
785 .run(Request::builder().method(Method::POST).uri("/login").build())
786 .await;
787
788 assert_eq!(response.status(), StatusCode::OK);
789 }
790 }
791
792 #[tokio::test]
793 async fn the_limit_is_enforced_and_the_window_resets() {
794 let throttle = Arc::new(ThrottleRequests::new(2, Duration::from_millis(40)));
795
796 let send = || {
797 let throttle = Arc::clone(&throttle);
798 async move {
799 Pipeline::new()
800 .through_arc(throttle as Arc<dyn Middleware>)
801 .then(|_| async { Response::ok("through") })
802 .run(Request::builder().header("authorization", "Bearer a").build())
803 .await
804 }
805 };
806
807 let first = send().await;
808 assert_eq!(first.status(), StatusCode::OK);
809 assert_eq!(first.header("x-ratelimit-remaining"), Some("1"));
810
811 assert_eq!(send().await.header("x-ratelimit-remaining"), Some("0"));
812
813 let refused = send().await;
814 assert_eq!(refused.status(), StatusCode::TOO_MANY_REQUESTS);
815 assert!(refused.header("retry-after").is_some());
816
817 tokio::time::sleep(Duration::from_millis(60)).await;
818 assert_eq!(send().await.status(), StatusCode::OK, "the window should have rolled over");
819 }
820
821 #[tokio::test]
822 async fn an_unreachable_store_fails_open() {
823 struct Broken;
827
828 impl RateLimitStore for Broken {
829 fn hit<'a>(
830 &'a self,
831 _key: &'a str,
832 _window: Duration,
833 ) -> rainier_support::BoxFuture<'a, rainier_support::Result<crate::rate_limit::Hit>>
834 {
835 Box::pin(async { Err(rainier_support::Error::internal("connection refused")) })
836 }
837
838 fn peek<'a>(
839 &'a self,
840 _key: &'a str,
841 ) -> rainier_support::BoxFuture<
842 'a,
843 rainier_support::Result<Option<crate::rate_limit::Hit>>,
844 > {
845 Box::pin(async { Ok(None) })
846 }
847
848 fn clear<'a>(
849 &'a self,
850 _key: &'a str,
851 ) -> rainier_support::BoxFuture<'a, rainier_support::Result<()>> {
852 Box::pin(async { Ok(()) })
853 }
854
855 fn is_shared(&self) -> bool {
856 true
857 }
858
859 fn name(&self) -> &str {
860 "broken"
861 }
862 }
863
864 let response = Pipeline::new()
865 .through(ThrottleRequests::per_minute(1).stored_in(Arc::new(Broken)))
866 .then(|_| async { Response::ok("through") })
867 .run(Request::builder().header("authorization", "Bearer a").build())
868 .await;
869
870 assert_eq!(response.status(), StatusCode::OK);
871 }
872
873 #[test]
874 fn a_memory_backed_throttle_says_it_is_not_shared() {
875 assert!(!ThrottleRequests::per_minute(1).is_shared());
876 assert_eq!(ThrottleRequests::per_minute(5).max_attempts(), 5);
877 assert_eq!(ThrottleRequests::per_minute(5).named("x").limiter_name(), Some("x"));
878 }
879
880 #[tokio::test]
881 async fn a_throttled_response_is_a_framework_error_body() {
882 let response = rainier_support::Error::new(
883 rainier_support::ErrorKind::TooManyRequests,
884 "Too many requests.",
885 )
886 .into_response();
887 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
888 }
889}