1use anyhow::{Context, Result, anyhow};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Clone)]
12pub struct RelayClient {
13 base_url: String,
14 client: reqwest::blocking::Client,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct AllocateResponse {
19 pub slot_id: String,
20 pub slot_token: String,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct PostEventResponse {
25 pub event_id: Option<String>,
26 pub status: String,
27}
28
29pub const INSECURE_SKIP_TLS_ENV: &str = "WIRE_INSECURE_SKIP_TLS_VERIFY";
37
38fn insecure_skip_tls_verify() -> bool {
39 matches!(
40 std::env::var(INSECURE_SKIP_TLS_ENV)
41 .unwrap_or_default()
42 .to_ascii_lowercase()
43 .as_str(),
44 "1" | "true" | "yes" | "on"
45 )
46}
47
48fn maybe_emit_insecure_banner() {
53 static ONCE: std::sync::OnceLock<()> = std::sync::OnceLock::new();
54 if insecure_skip_tls_verify() {
55 ONCE.get_or_init(|| {
56 eprintln!(
57 "\x1b[1;31mwire: WARNING\x1b[0m {INSECURE_SKIP_TLS_ENV}=1 is set; TLS verification is DISABLED for all relay traffic. \
58 MITM attacks against the relay path are undetectable in this mode. Unset to restore default trust validation."
59 );
60 });
61 }
62}
63
64pub fn build_blocking_client(
70 timeout: Option<std::time::Duration>,
71) -> Result<reqwest::blocking::Client> {
72 let mut b = reqwest::blocking::Client::builder();
73 if let Some(t) = timeout {
74 b = b.timeout(t);
75 }
76 if insecure_skip_tls_verify() {
77 maybe_emit_insecure_banner();
78 b = b.danger_accept_invalid_certs(true);
79 }
80 b.build()
81 .with_context(|| "constructing reqwest blocking client")
82}
83
84pub fn format_transport_error(err: &anyhow::Error) -> String {
92 let mut parts: Vec<String> = err.chain().map(|c| c.to_string()).collect();
93 let lower = parts
97 .iter()
98 .map(|p| p.to_ascii_lowercase())
99 .collect::<Vec<_>>();
100 let class = if lower.iter().any(|p| {
101 p.contains("invalid peer certificate")
102 || p.contains("certificate verification")
103 || p.contains("unknownissuer")
104 || p.contains("certificate is not valid")
105 || p.contains("tls handshake")
106 }) {
107 Some("TLS error")
108 } else if lower.iter().any(|p| {
109 p.contains("dns error")
110 || p.contains("nodename nor servname")
111 || p.contains("failed to lookup address")
112 }) {
113 Some("DNS error")
114 } else if lower
115 .iter()
116 .any(|p| p.contains("operation timed out") || p.contains("deadline has elapsed"))
117 {
118 Some("timeout")
119 } else if lower
120 .iter()
121 .any(|p| p.contains("connection refused") || p.contains("connection reset"))
122 {
123 Some("connect error")
124 } else {
125 None
126 };
127 if let Some(c) = class {
128 parts.insert(0, c.to_string());
129 }
130 parts.join(": ")
131}
132
133#[cfg(unix)]
145pub fn uds_request(
146 socket_path: &std::path::Path,
147 method: &str,
148 request_target: &str,
149 headers: &[(&str, &str)],
150 body: &[u8],
151) -> Result<(u16, Vec<u8>)> {
152 use std::io::{Read, Write};
153 use std::os::unix::net::UnixStream;
154 let mut stream =
155 UnixStream::connect(socket_path).with_context(|| format!("connect UDS {socket_path:?}"))?;
156 stream.set_read_timeout(Some(std::time::Duration::from_secs(30)))?;
157 stream.set_write_timeout(Some(std::time::Duration::from_secs(30)))?;
158 let mut req = String::with_capacity(256 + headers.len() * 32 + body.len());
159 req.push_str(method);
160 req.push(' ');
161 req.push_str(request_target);
162 req.push_str(" HTTP/1.1\r\n");
163 req.push_str("Host: localhost\r\n");
164 req.push_str("Connection: close\r\n");
165 req.push_str(&format!("Content-Length: {}\r\n", body.len()));
166 for (k, v) in headers {
167 req.push_str(k);
168 req.push_str(": ");
169 req.push_str(v);
170 req.push_str("\r\n");
171 }
172 req.push_str("\r\n");
173 stream.write_all(req.as_bytes())?;
174 if !body.is_empty() {
175 stream.write_all(body)?;
176 }
177 stream.flush()?;
178 let mut raw = Vec::new();
179 stream.read_to_end(&mut raw)?;
180 let split = raw
182 .windows(4)
183 .position(|w| w == b"\r\n\r\n")
184 .ok_or_else(|| anyhow!("UDS response missing header/body delimiter"))?;
185 let head = std::str::from_utf8(&raw[..split])
186 .map_err(|e| anyhow!("UDS response head not UTF-8: {e}"))?;
187 let body = raw[split + 4..].to_vec();
188 let status_line = head.lines().next().unwrap_or("");
189 let status: u16 = status_line
191 .split_whitespace()
192 .nth(1)
193 .and_then(|s| s.parse().ok())
194 .ok_or_else(|| anyhow!("UDS response missing status code: {status_line:?}"))?;
195 Ok((status, body))
196}
197
198pub fn post_event_to_endpoint(
209 endpoint: &crate::endpoints::Endpoint,
210 event: &Value,
211) -> Result<PostEventResponse> {
212 #[cfg(unix)]
213 if let Some(socket_path) = endpoint.relay_url.strip_prefix("unix://") {
214 let body = serde_json::json!({"event": event}).to_string();
215 let auth_header = format!("Bearer {}", endpoint.slot_token);
216 let (status, body) = uds_request(
217 std::path::Path::new(socket_path),
218 "POST",
219 &format!("/v1/events/{}", endpoint.slot_id),
220 &[
221 ("Content-Type", "application/json"),
222 ("Authorization", &auth_header),
223 ],
224 body.as_bytes(),
225 )?;
226 if !(200..300).contains(&status) {
227 return Err(anyhow!(
228 "post_event (uds {socket_path}) failed: {status}: {}",
229 String::from_utf8_lossy(&body)
230 ));
231 }
232 return Ok(serde_json::from_slice(&body)?);
233 }
234 let client = RelayClient::new(&endpoint.relay_url);
235 client.post_event(&endpoint.slot_id, &endpoint.slot_token, event)
236}
237
238impl RelayClient {
239 pub fn new(base_url: &str) -> Self {
240 let client = build_blocking_client(Some(std::time::Duration::from_secs(30)))
241 .expect("reqwest client construction is infallible with rustls + native roots");
242 Self {
243 base_url: base_url.trim_end_matches('/').to_string(),
244 client,
245 }
246 }
247
248 pub fn allocate_slot(&self, handle_hint: Option<&str>) -> Result<AllocateResponse> {
252 let body = serde_json::json!({"handle": handle_hint});
253 let resp = self
254 .client
255 .post(format!("{}/v1/slot/allocate", self.base_url))
256 .json(&body)
257 .send()
258 .with_context(|| format!("POST {}/v1/slot/allocate", self.base_url))?;
259 let status = resp.status();
260 if !status.is_success() {
261 let detail = resp.text().unwrap_or_default();
262 return Err(anyhow!("allocate failed: {status}: {detail}"));
263 }
264 Ok(resp.json()?)
265 }
266
267 pub fn post_event(
271 &self,
272 slot_id: &str,
273 slot_token: &str,
274 event: &Value,
275 ) -> Result<PostEventResponse> {
276 let body = serde_json::json!({"event": event});
277 let resp = self
278 .client
279 .post(format!("{}/v1/events/{slot_id}", self.base_url))
280 .bearer_auth(slot_token)
281 .json(&body)
282 .send()
283 .with_context(|| format!("POST {}/v1/events/{slot_id}", self.base_url))?;
284 let status = resp.status();
285 if !status.is_success() {
286 let detail = resp.text().unwrap_or_default();
287 return Err(anyhow!("post_event failed: {status}: {detail}"));
288 }
289 Ok(resp.json()?)
290 }
291
292 pub fn list_events(
295 &self,
296 slot_id: &str,
297 slot_token: &str,
298 since: Option<&str>,
299 limit: Option<usize>,
300 ) -> Result<Vec<Value>> {
301 let mut url = format!("{}/v1/events/{slot_id}", self.base_url);
302 let mut sep = '?';
303 if let Some(s) = since {
304 url.push(sep);
305 url.push_str(&format!("since={s}"));
306 sep = '&';
307 }
308 if let Some(n) = limit {
309 url.push(sep);
310 url.push_str(&format!("limit={n}"));
311 }
312 let resp = self
313 .client
314 .get(&url)
315 .bearer_auth(slot_token)
316 .send()
317 .with_context(|| format!("GET {url}"))?;
318 let status = resp.status();
319 if !status.is_success() {
320 let detail = resp.text().unwrap_or_default();
321 return Err(anyhow!("list_events failed: {status}: {detail}"));
322 }
323 Ok(resp.json()?)
324 }
325
326 pub fn slot_state(&self, slot_id: &str, slot_token: &str) -> Result<(usize, Option<u64>)> {
332 let url = format!("{}/v1/slot/{slot_id}/state", self.base_url);
333 let resp = match self.client.get(&url).bearer_auth(slot_token).send() {
334 Ok(r) => r,
335 Err(_) => return Ok((0, None)),
336 };
337 if !resp.status().is_success() {
338 return Ok((0, None));
339 }
340 let v: Value = resp.json().unwrap_or(Value::Null);
341 let count = v.get("event_count").and_then(Value::as_u64).unwrap_or(0) as usize;
342 let last = v.get("last_pull_at_unix").and_then(Value::as_u64);
343 Ok((count, last))
344 }
345
346 pub fn responder_health_set(
347 &self,
348 slot_id: &str,
349 slot_token: &str,
350 record: &Value,
351 ) -> Result<Value> {
352 let resp = self
353 .client
354 .post(format!(
355 "{}/v1/slot/{slot_id}/responder-health",
356 self.base_url
357 ))
358 .bearer_auth(slot_token)
359 .json(record)
360 .send()
361 .with_context(|| {
362 format!("POST {}/v1/slot/{slot_id}/responder-health", self.base_url)
363 })?;
364 let status = resp.status();
365 if !status.is_success() {
366 let detail = resp.text().unwrap_or_default();
367 return Err(anyhow!("responder_health_set failed: {status}: {detail}"));
368 }
369 Ok(resp.json()?)
370 }
371
372 pub fn responder_health_get(&self, slot_id: &str, slot_token: &str) -> Result<Value> {
373 let resp = self
374 .client
375 .get(format!("{}/v1/slot/{slot_id}/state", self.base_url))
376 .bearer_auth(slot_token)
377 .send()
378 .with_context(|| format!("GET {}/v1/slot/{slot_id}/state", self.base_url))?;
379 let status = resp.status();
380 if !status.is_success() {
381 let detail = resp.text().unwrap_or_default();
382 return Err(anyhow!("responder_health_get failed: {status}: {detail}"));
383 }
384 let state: Value = resp.json()?;
385 Ok(state
386 .get("responder_health")
387 .cloned()
388 .unwrap_or(Value::Null))
389 }
390
391 pub fn healthz(&self) -> Result<bool> {
392 let resp = self
393 .client
394 .get(format!("{}/healthz", self.base_url))
395 .send()?;
396 Ok(resp.status().is_success())
397 }
398
399 pub fn check_healthz(&self) -> anyhow::Result<()> {
404 match self.healthz() {
405 Ok(true) => Ok(()),
406 Ok(false) => anyhow::bail!(
407 "phyllis: silent line — {}/healthz returned non-200.\n\
408 the host is reachable but the relay isn't returning ok. test:\n \
409 curl -v {}/healthz",
410 self.base_url,
411 self.base_url
412 ),
413 Err(e) => anyhow::bail!(
414 "phyllis: silent line — couldn't reach {}/healthz: {e:#}.\n\
415 test reachability from this machine:\n curl -v {}/healthz\n\
416 if curl also fails, a sandbox / proxy / firewall is the usual cause.\n\
417 (OpenShell sandbox? run `curl -fsSL https://wireup.net/openshell-policy.sh | bash -s <sandbox-name>` on the host first.)",
418 self.base_url,
419 self.base_url
420 ),
421 }
422 }
423
424 pub fn pair_open(&self, code_hash: &str, msg_b64: &str, role: &str) -> Result<String> {
428 let body = serde_json::json!({"code_hash": code_hash, "msg": msg_b64, "role": role});
429 let resp = self
430 .client
431 .post(format!("{}/v1/pair", self.base_url))
432 .json(&body)
433 .send()?;
434 let status = resp.status();
435 if !status.is_success() {
436 let detail = resp.text().unwrap_or_default();
437 return Err(anyhow!("pair_open failed: {status}: {detail}"));
438 }
439 let v: Value = resp.json()?;
440 v.get("pair_id")
441 .and_then(Value::as_str)
442 .map(str::to_string)
443 .ok_or_else(|| anyhow!("pair_open response missing pair_id"))
444 }
445
446 pub fn pair_abandon(&self, code_hash: &str) -> Result<()> {
451 let body = serde_json::json!({"code_hash": code_hash});
452 let resp = self
453 .client
454 .post(format!("{}/v1/pair/abandon", self.base_url))
455 .json(&body)
456 .send()?;
457 let status = resp.status();
458 if !status.is_success() {
459 let detail = resp.text().unwrap_or_default();
460 return Err(anyhow!("pair_abandon failed: {status}: {detail}"));
461 }
462 Ok(())
463 }
464
465 pub fn pair_get(
467 &self,
468 pair_id: &str,
469 as_role: &str,
470 ) -> Result<(Option<String>, Option<String>)> {
471 let resp = self
472 .client
473 .get(format!(
474 "{}/v1/pair/{pair_id}?as_role={as_role}",
475 self.base_url
476 ))
477 .send()?;
478 let status = resp.status();
479 if !status.is_success() {
480 let detail = resp.text().unwrap_or_default();
481 return Err(anyhow!("pair_get failed: {status}: {detail}"));
482 }
483 let v: Value = resp.json()?;
484 let peer_msg = v
485 .get("peer_msg")
486 .and_then(Value::as_str)
487 .map(str::to_string);
488 let peer_bootstrap = v
489 .get("peer_bootstrap")
490 .and_then(Value::as_str)
491 .map(str::to_string);
492 Ok((peer_msg, peer_bootstrap))
493 }
494
495 pub fn pair_bootstrap(&self, pair_id: &str, role: &str, sealed_b64: &str) -> Result<()> {
497 let body = serde_json::json!({"role": role, "sealed": sealed_b64});
498 let resp = self
499 .client
500 .post(format!("{}/v1/pair/{pair_id}/bootstrap", self.base_url))
501 .json(&body)
502 .send()?;
503 if !resp.status().is_success() {
504 let s = resp.status();
505 let detail = resp.text().unwrap_or_default();
506 return Err(anyhow!("pair_bootstrap failed: {s}: {detail}"));
507 }
508 Ok(())
509 }
510
511 pub fn handle_claim(
517 &self,
518 nick: &str,
519 slot_id: &str,
520 slot_token: &str,
521 relay_url: Option<&str>,
522 card: &Value,
523 ) -> Result<Value> {
524 self.handle_claim_v2(nick, slot_id, slot_token, relay_url, card, None)
525 }
526
527 pub fn handle_claim_v2(
533 &self,
534 nick: &str,
535 slot_id: &str,
536 slot_token: &str,
537 relay_url: Option<&str>,
538 card: &Value,
539 discoverable: Option<bool>,
540 ) -> Result<Value> {
541 let mut body = serde_json::json!({
542 "nick": nick,
543 "slot_id": slot_id,
544 "relay_url": relay_url,
545 "card": card,
546 });
547 if let Some(d) = discoverable {
548 body["discoverable"] = serde_json::json!(d);
549 }
550 let resp = self
551 .client
552 .post(format!("{}/v1/handle/claim", self.base_url))
553 .bearer_auth(slot_token)
554 .json(&body)
555 .send()
556 .with_context(|| format!("POST {}/v1/handle/claim", self.base_url))?;
557 let status = resp.status();
558 if !status.is_success() {
559 let detail = resp.text().unwrap_or_default();
560 return Err(anyhow!("handle_claim failed: {status}: {detail}"));
561 }
562 Ok(resp.json()?)
563 }
564
565 pub fn handle_intro(&self, nick: &str, event: &Value) -> Result<Value> {
569 let body = serde_json::json!({"event": event});
570 let resp = self
571 .client
572 .post(format!("{}/v1/handle/intro/{nick}", self.base_url))
573 .json(&body)
574 .send()
575 .with_context(|| format!("POST {}/v1/handle/intro/{nick}", self.base_url))?;
576 let status = resp.status();
577 if !status.is_success() {
578 let detail = resp.text().unwrap_or_default();
579 return Err(anyhow!("handle_intro failed: {status}: {detail}"));
580 }
581 Ok(resp.json()?)
582 }
583
584 pub fn well_known_agent_card_a2a(&self, handle: &str) -> Result<Value> {
590 let resp = self
591 .client
592 .get(format!("{}/.well-known/agent-card.json", self.base_url))
593 .query(&[("handle", handle)])
594 .send()
595 .with_context(|| {
596 format!(
597 "GET {}/.well-known/agent-card.json?handle={handle}",
598 self.base_url
599 )
600 })?;
601 let status = resp.status();
602 if !status.is_success() {
603 let detail = resp.text().unwrap_or_default();
604 return Err(anyhow!(
605 "well_known_agent_card_a2a failed: {status}: {detail}"
606 ));
607 }
608 Ok(resp.json()?)
609 }
610
611 pub fn well_known_agent(&self, handle: &str) -> Result<Value> {
615 let resp = self
616 .client
617 .get(format!("{}/.well-known/wire/agent", self.base_url))
618 .query(&[("handle", handle)])
619 .send()
620 .with_context(|| {
621 format!(
622 "GET {}/.well-known/wire/agent?handle={handle}",
623 self.base_url
624 )
625 })?;
626 let status = resp.status();
627 if !status.is_success() {
628 let detail = resp.text().unwrap_or_default();
629 return Err(anyhow!("well_known_agent failed: {status}: {detail}"));
630 }
631 Ok(resp.json()?)
632 }
633}
634
635#[cfg(all(test, unix))]
636mod uds_tests {
637 use super::*;
638 use std::io::{Read, Write};
639 use std::os::unix::net::UnixListener;
640 use std::thread;
641
642 fn spawn_canned_uds_server(socket_path: std::path::PathBuf, status: u16, body: &'static str) {
646 let listener = UnixListener::bind(&socket_path).expect("bind canned UDS");
647 thread::spawn(move || {
648 let (mut stream, _) = listener.accept().expect("accept canned UDS");
649 let mut req_buf = [0u8; 4096];
650 let _ = stream.read(&mut req_buf);
651 let body_bytes = body.as_bytes();
652 let status_text = match status {
653 200 => "OK",
654 201 => "Created",
655 400 => "Bad Request",
656 _ => "Status",
657 };
658 let resp = format!(
659 "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
660 body_bytes.len()
661 );
662 let _ = stream.write_all(resp.as_bytes());
663 });
664 }
665
666 #[test]
667 fn uds_request_round_trips_200_with_body() {
668 let tmpdir = std::env::temp_dir().join(format!("wire-uds-test-{}", rand::random::<u32>()));
669 std::fs::create_dir_all(&tmpdir).unwrap();
670 let sock = tmpdir.join("rt.sock");
671 let _ = std::fs::remove_file(&sock);
672 spawn_canned_uds_server(sock.clone(), 200, r#"{"ok":true}"#);
673 std::thread::sleep(std::time::Duration::from_millis(50));
675 let (status, body) = uds_request(
676 &sock,
677 "POST",
678 "/v1/test",
679 &[("Content-Type", "application/json")],
680 b"{}",
681 )
682 .expect("uds_request succeeds");
683 assert_eq!(status, 200);
684 assert_eq!(body, br#"{"ok":true}"#);
685 }
686
687 #[test]
688 fn uds_request_surfaces_non_2xx_status() {
689 let tmpdir = std::env::temp_dir().join(format!("wire-uds-test-{}", rand::random::<u32>()));
690 std::fs::create_dir_all(&tmpdir).unwrap();
691 let sock = tmpdir.join("err.sock");
692 let _ = std::fs::remove_file(&sock);
693 spawn_canned_uds_server(sock.clone(), 400, r#"{"error":"bad"}"#);
694 std::thread::sleep(std::time::Duration::from_millis(50));
695 let (status, body) = uds_request(&sock, "GET", "/v1/test", &[], b"")
696 .expect("uds_request succeeds even on 4xx");
697 assert_eq!(status, 400);
698 assert_eq!(body, br#"{"error":"bad"}"#);
699 }
700
701 #[test]
702 fn uds_request_fails_on_nonexistent_socket() {
703 let nope = std::path::Path::new("/tmp/wire-uds-nonexistent-socket-aaa.sock");
704 let _ = std::fs::remove_file(nope);
705 let err = uds_request(nope, "GET", "/", &[], b"").unwrap_err();
706 let msg = format!("{err:#}");
707 assert!(
708 msg.contains("connect UDS"),
709 "expected connect error, got: {msg}"
710 );
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn url_normalization_trims_trailing_slash() {
720 let c = RelayClient::new("http://example.com/");
721 assert_eq!(c.base_url, "http://example.com");
722 let c = RelayClient::new("http://example.com");
723 assert_eq!(c.base_url, "http://example.com");
724 }
725
726 #[test]
727 fn format_transport_error_classifies_tls() {
728 let inner = anyhow!("invalid peer certificate: UnknownIssuer");
731 let middle: anyhow::Error = inner.context("hyper send");
732 let top = middle.context("POST https://relay.example/v1/events/abc");
733 let formatted = format_transport_error(&top);
734 assert!(
735 formatted.starts_with("TLS error:"),
736 "expected TLS class prefix, got: {formatted}"
737 );
738 assert!(
739 formatted.contains("UnknownIssuer"),
740 "lost root cause: {formatted}"
741 );
742 assert!(
743 formatted.contains("POST https://relay.example"),
744 "lost context URL: {formatted}"
745 );
746 }
747
748 #[test]
749 fn format_transport_error_classifies_timeout() {
750 let inner = anyhow!("operation timed out");
751 let top = inner.context("POST https://relay.example/v1/events/abc");
752 let formatted = format_transport_error(&top);
753 assert!(formatted.starts_with("timeout:"), "got: {formatted}");
754 }
755
756 #[test]
757 fn format_transport_error_classifies_dns() {
758 let inner = anyhow!("dns error: failed to lookup address");
759 let top = inner.context("POST https://relay.example/v1/events/abc");
760 let formatted = format_transport_error(&top);
761 assert!(formatted.starts_with("DNS error:"), "got: {formatted}");
762 }
763
764 #[test]
765 fn format_transport_error_falls_back_to_chain_join() {
766 let inner = anyhow!("Refused to connect for non-standard reason xyz");
769 let top = inner.context("POST https://relay.example/v1/events/abc");
770 let formatted = format_transport_error(&top);
771 assert!(formatted.contains("Refused to connect"));
772 assert!(formatted.contains("POST https://relay.example"));
773 }
774
775 #[test]
776 fn insecure_env_recognizes_truthy_values_and_default_off() {
777 use std::sync::{Mutex, OnceLock};
781 static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
782 let _lock = GUARD.get_or_init(|| Mutex::new(())).lock().unwrap();
783
784 unsafe {
787 std::env::remove_var(INSECURE_SKIP_TLS_ENV);
788 }
789 assert!(!insecure_skip_tls_verify(), "default must be secure");
790
791 for v in ["1", "true", "yes", "on", "TRUE", "Yes"] {
792 unsafe {
793 std::env::set_var(INSECURE_SKIP_TLS_ENV, v);
794 }
795 assert!(insecure_skip_tls_verify(), "value {v:?} should be truthy");
796 }
797 for v in ["0", "false", "no", "off", ""] {
799 unsafe {
800 std::env::set_var(INSECURE_SKIP_TLS_ENV, v);
801 }
802 assert!(
803 !insecure_skip_tls_verify(),
804 "value {v:?} must not enable insecure mode"
805 );
806 }
807 unsafe {
808 std::env::remove_var(INSECURE_SKIP_TLS_ENV);
809 }
810 }
811}