1use crate::err::ErrorDetail;
5use crate::StreamPrefs;
6use std::fmt::Display;
7use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
8use std::str::FromStr;
9use thiserror::Error;
10use tor_basic_utils::StrExt;
11use tor_error::{ErrorKind, HasKind};
12
13#[cfg(feature = "onion-service-client")]
14use tor_hscrypto::pk::{HsId, HSID_ONION_SUFFIX};
15
16#[cfg(not(feature = "onion-service-client"))]
18pub(crate) mod hs_dummy {
19 use super::*;
20 use tor_error::internal;
21 use void::Void;
22
23 #[derive(Debug, Clone)]
25 pub(crate) struct HsId(pub(crate) Void);
26
27 impl PartialEq for HsId {
28 fn eq(&self, _other: &Self) -> bool {
29 void::unreachable(self.0)
30 }
31 }
32 impl Eq for HsId {}
33
34 pub(crate) const HSID_ONION_SUFFIX: &str = ".onion";
36
37 impl FromStr for HsId {
39 type Err = ErrorDetail;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 if !s.ends_with(HSID_ONION_SUFFIX) {
43 return Err(internal!("non-.onion passed to dummy HsId::from_str").into());
44 }
45
46 Err(ErrorDetail::OnionAddressNotSupported)
47 }
48 }
49}
50#[cfg(not(feature = "onion-service-client"))]
51use hs_dummy::*;
52
53pub trait IntoTorAddr {
78 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError>;
81}
82
83pub trait DangerouslyIntoTorAddr {
95 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError>;
101}
102
103#[derive(Debug, Clone, Eq, PartialEq)]
160pub struct TorAddr {
161 host: Host,
163 port: u16,
165}
166
167#[derive(Debug, PartialEq, Eq)]
173pub(crate) enum StreamInstructions {
174 Exit {
176 hostname: String,
178 port: u16,
180 },
181 Hs {
185 hsid: HsId,
187 hostname: String,
189 port: u16,
191 },
192}
193
194#[derive(PartialEq, Eq, Debug)]
196pub(crate) enum ResolveInstructions {
197 Exit(String),
199 Return(Vec<IpAddr>),
201}
202
203impl TorAddr {
204 fn new(host: Host, port: u16) -> Result<Self, TorAddrError> {
207 if port == 0 {
208 Err(TorAddrError::BadPort)
209 } else {
210 Ok(TorAddr { host, port })
211 }
212 }
213
214 pub fn from<A: IntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
217 addr.into_tor_addr()
218 }
219 pub fn dangerously_from<A: DangerouslyIntoTorAddr>(addr: A) -> Result<Self, TorAddrError> {
226 addr.into_tor_addr_dangerously()
227 }
228
229 pub fn is_ip_address(&self) -> bool {
231 matches!(&self.host, Host::Ip(_))
232 }
233
234 pub(crate) fn into_stream_instructions(
236 self,
237 cfg: &crate::config::ClientAddrConfig,
238 prefs: &StreamPrefs,
239 ) -> Result<StreamInstructions, ErrorDetail> {
240 self.enforce_config(cfg, prefs)?;
241
242 let port = self.port;
243 Ok(match self.host {
244 Host::Hostname(hostname) => StreamInstructions::Exit { hostname, port },
245 Host::Ip(ip) => StreamInstructions::Exit {
246 hostname: ip.to_string(),
247 port,
248 },
249 Host::Onion(onion) => {
250 let rhs = onion
252 .rmatch_indices('.')
253 .nth(1)
254 .map(|(i, _)| i + 1)
255 .unwrap_or(0);
256 let rhs = &onion[rhs..];
257 let hsid = rhs.parse()?;
258 StreamInstructions::Hs {
259 hsid,
260 port,
261 hostname: onion,
262 }
263 }
264 })
265 }
266
267 pub(crate) fn into_resolve_instructions(
269 self,
270 cfg: &crate::config::ClientAddrConfig,
271 prefs: &StreamPrefs,
272 ) -> Result<ResolveInstructions, ErrorDetail> {
273 let enforce_config_result = self.enforce_config(cfg, prefs);
278
279 let instructions = (move || {
282 Ok(match self.host {
283 Host::Hostname(hostname) => ResolveInstructions::Exit(hostname),
284 Host::Ip(ip) => ResolveInstructions::Return(vec![ip]),
285 Host::Onion(_) => return Err(ErrorDetail::OnionAddressResolveRequest),
286 })
287 })()?;
288
289 let () = enforce_config_result?;
290
291 Ok(instructions)
292 }
293
294 fn is_local(&self) -> bool {
296 self.host.is_local()
297 }
298
299 fn enforce_config(
302 &self,
303 cfg: &crate::config::ClientAddrConfig,
304 #[allow(unused_variables)] prefs: &StreamPrefs,
306 ) -> Result<(), ErrorDetail> {
307 if !cfg.allow_local_addrs && self.is_local() {
308 return Err(ErrorDetail::LocalAddress);
309 }
310
311 if let Host::Hostname(addr) = &self.host {
312 if !is_valid_hostname(addr) {
313 return Err(ErrorDetail::InvalidHostname);
315 }
316 if addr.ends_with_ignore_ascii_case(HSID_ONION_SUFFIX) {
317 return Err(ErrorDetail::OnionAddressNotSupported);
319 }
320 }
321
322 if let Host::Onion(_name) = &self.host {
323 cfg_if::cfg_if! {
324 if #[cfg(feature = "onion-service-client")] {
325 if !prefs.connect_to_onion_services.as_bool().unwrap_or(cfg.allow_onion_addrs) {
326 return Err(ErrorDetail::OnionAddressDisabled);
327 }
328 } else {
329 return Err(ErrorDetail::OnionAddressNotSupported);
330 }
331 }
332 }
333
334 Ok(())
335 }
336}
337
338impl std::fmt::Display for TorAddr {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 match self.host {
341 Host::Ip(IpAddr::V6(addr)) => write!(f, "[{}]:{}", addr, self.port),
342 _ => write!(f, "{}:{}", self.host, self.port),
343 }
344 }
345}
346
347#[derive(Debug, Error, Clone, Eq, PartialEq)]
352#[non_exhaustive]
353pub enum TorAddrError {
354 #[error("String can never be a valid hostname")]
356 InvalidHostname,
357 #[error("No port found in string")]
359 NoPort,
360 #[error("Could not parse port")]
362 BadPort,
363}
364
365impl HasKind for TorAddrError {
366 fn kind(&self) -> ErrorKind {
367 use ErrorKind as EK;
368 use TorAddrError as TAE;
369
370 match self {
371 TAE::InvalidHostname => EK::InvalidStreamTarget,
372 TAE::NoPort => EK::InvalidStreamTarget,
373 TAE::BadPort => EK::InvalidStreamTarget,
374 }
375 }
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
388enum Host {
389 Hostname(String),
401 Ip(IpAddr),
403 Onion(String),
408}
409
410impl FromStr for Host {
411 type Err = TorAddrError;
412 fn from_str(s: &str) -> Result<Host, TorAddrError> {
413 if s.ends_with_ignore_ascii_case(".onion") && is_valid_hostname(s) {
414 Ok(Host::Onion(s.to_owned()))
415 } else if let Ok(ip_addr) = s.parse() {
416 Ok(Host::Ip(ip_addr))
417 } else if is_valid_hostname(s) {
418 Ok(Host::Hostname(s.to_owned()))
424 } else {
425 Err(TorAddrError::InvalidHostname)
426 }
427 }
428}
429
430impl Host {
431 fn is_local(&self) -> bool {
434 match self {
435 Host::Hostname(name) => name.eq_ignore_ascii_case("localhost"),
436 Host::Ip(IpAddr::V4(ip)) => ip.is_loopback() || ip.is_private(),
443 Host::Ip(IpAddr::V6(ip)) => ip.is_loopback(),
444 Host::Onion(_) => false,
445 }
446 }
447}
448
449impl std::fmt::Display for Host {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 match self {
452 Host::Hostname(s) => Display::fmt(s, f),
453 Host::Ip(ip) => Display::fmt(ip, f),
454 Host::Onion(onion) => Display::fmt(onion, f),
455 }
456 }
457}
458
459impl IntoTorAddr for TorAddr {
460 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
461 Ok(self)
462 }
463}
464
465impl<A: IntoTorAddr + Clone> IntoTorAddr for &A {
466 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
467 self.clone().into_tor_addr()
468 }
469}
470
471impl IntoTorAddr for &str {
472 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
473 if let Ok(sa) = SocketAddr::from_str(self) {
474 TorAddr::new(Host::Ip(sa.ip()), sa.port())
475 } else {
476 let (host, port) = self.rsplit_once(':').ok_or(TorAddrError::NoPort)?;
477 let host = host.parse()?;
478 let port = port.parse().map_err(|_| TorAddrError::BadPort)?;
479 TorAddr::new(host, port)
480 }
481 }
482}
483
484impl IntoTorAddr for String {
485 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
486 self[..].into_tor_addr()
487 }
488}
489
490impl FromStr for TorAddr {
491 type Err = TorAddrError;
492 fn from_str(s: &str) -> Result<Self, TorAddrError> {
493 s.into_tor_addr()
494 }
495}
496
497impl IntoTorAddr for (&str, u16) {
498 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
499 let (host, port) = self;
500 let host = host.parse()?;
501 TorAddr::new(host, port)
502 }
503}
504
505impl IntoTorAddr for (String, u16) {
506 fn into_tor_addr(self) -> Result<TorAddr, TorAddrError> {
507 let (host, port) = self;
508 (&host[..], port).into_tor_addr()
509 }
510}
511
512impl<T: DangerouslyIntoTorAddr + Clone> DangerouslyIntoTorAddr for &T {
513 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
514 self.clone().into_tor_addr_dangerously()
515 }
516}
517
518impl DangerouslyIntoTorAddr for (IpAddr, u16) {
519 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
520 let (addr, port) = self;
521 TorAddr::new(Host::Ip(addr), port)
522 }
523}
524
525impl DangerouslyIntoTorAddr for (Ipv4Addr, u16) {
526 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
527 let (addr, port) = self;
528 TorAddr::new(Host::Ip(addr.into()), port)
529 }
530}
531
532impl DangerouslyIntoTorAddr for (Ipv6Addr, u16) {
533 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
534 let (addr, port) = self;
535 TorAddr::new(Host::Ip(addr.into()), port)
536 }
537}
538
539impl DangerouslyIntoTorAddr for SocketAddr {
540 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
541 let (addr, port) = (self.ip(), self.port());
542 (addr, port).into_tor_addr_dangerously()
543 }
544}
545
546impl DangerouslyIntoTorAddr for SocketAddrV4 {
547 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
548 let (addr, port) = (self.ip(), self.port());
549 (*addr, port).into_tor_addr_dangerously()
550 }
551}
552
553impl DangerouslyIntoTorAddr for SocketAddrV6 {
554 fn into_tor_addr_dangerously(self) -> Result<TorAddr, TorAddrError> {
555 let (addr, port) = (self.ip(), self.port());
556 (*addr, port).into_tor_addr_dangerously()
557 }
558}
559
560fn is_valid_hostname(hostname: &str) -> bool {
564 hostname_validator::is_valid(hostname)
565}
566
567#[cfg(test)]
568mod test {
569 #![allow(clippy::bool_assert_comparison)]
571 #![allow(clippy::clone_on_copy)]
572 #![allow(clippy::dbg_macro)]
573 #![allow(clippy::mixed_attributes_style)]
574 #![allow(clippy::print_stderr)]
575 #![allow(clippy::print_stdout)]
576 #![allow(clippy::single_char_pattern)]
577 #![allow(clippy::unwrap_used)]
578 #![allow(clippy::unchecked_duration_subtraction)]
579 #![allow(clippy::useless_vec)]
580 #![allow(clippy::needless_pass_by_value)]
581 use super::*;
583
584 #[test]
585 fn test_error_kind() {
586 use tor_error::ErrorKind as EK;
587
588 assert_eq!(
589 TorAddrError::InvalidHostname.kind(),
590 EK::InvalidStreamTarget
591 );
592 assert_eq!(TorAddrError::NoPort.kind(), EK::InvalidStreamTarget);
593 assert_eq!(TorAddrError::BadPort.kind(), EK::InvalidStreamTarget);
594 }
595
596 fn mk_stream_prefs() -> StreamPrefs {
598 let prefs = crate::StreamPrefs::default();
599
600 #[cfg(feature = "onion-service-client")]
601 let prefs = {
602 let mut prefs = prefs;
603 prefs.connect_to_onion_services(tor_config::BoolOrAuto::Explicit(true));
604 prefs
605 };
606
607 prefs
608 }
609
610 #[test]
611 fn validate_hostname() {
612 assert!(is_valid_hostname("torproject.org"));
614 assert!(is_valid_hostname("Tor-Project.org"));
615 assert!(is_valid_hostname("example.onion"));
616 assert!(is_valid_hostname("some.example.onion"));
617
618 assert!(!is_valid_hostname("-torproject.org"));
620 assert!(!is_valid_hostname("_torproject.org"));
621 assert!(!is_valid_hostname("tor_project1.org"));
622 assert!(!is_valid_hostname("iwanna$money.org"));
623 }
624
625 #[test]
626 fn validate_addr() {
627 use crate::err::ErrorDetail;
628 fn val<A: IntoTorAddr>(addr: A) -> Result<TorAddr, ErrorDetail> {
629 let toraddr = addr.into_tor_addr()?;
630 toraddr.enforce_config(&Default::default(), &mk_stream_prefs())?;
631 Ok(toraddr)
632 }
633
634 assert!(val("[2001:db8::42]:20").is_ok());
635 assert!(val(("2001:db8::42", 20)).is_ok());
636 assert!(val(("198.151.100.42", 443)).is_ok());
637 assert!(val("198.151.100.42:443").is_ok());
638 assert!(val("www.torproject.org:443").is_ok());
639 assert!(val(("www.torproject.org", 443)).is_ok());
640
641 #[cfg(feature = "onion-service-client")]
643 {
644 assert!(val("example.onion:80").is_ok());
645 assert!(val(("example.onion", 80)).is_ok());
646
647 match val("eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443") {
648 Ok(TorAddr {
649 host: Host::Onion(_),
650 ..
651 }) => {}
652 x => panic!("{x:?}"),
653 }
654 }
655
656 assert!(matches!(
657 val("-foobar.net:443"),
658 Err(ErrorDetail::InvalidHostname)
659 ));
660 assert!(matches!(
661 val("www.torproject.org"),
662 Err(ErrorDetail::Address(TorAddrError::NoPort))
663 ));
664
665 assert!(matches!(
666 val("192.168.0.1:80"),
667 Err(ErrorDetail::LocalAddress)
668 ));
669 assert!(matches!(
670 val(TorAddr::new(Host::Hostname("foo@bar".to_owned()), 553).unwrap()),
671 Err(ErrorDetail::InvalidHostname)
672 ));
673 assert!(matches!(
674 val(TorAddr::new(Host::Hostname("foo.onion".to_owned()), 80).unwrap()),
675 Err(ErrorDetail::OnionAddressNotSupported)
676 ));
677 }
678
679 #[test]
680 fn local_addrs() {
681 fn is_local_hostname(s: &str) -> bool {
682 let h: Host = s.parse().unwrap();
683 h.is_local()
684 }
685
686 assert!(is_local_hostname("localhost"));
687 assert!(is_local_hostname("loCALHOST"));
688 assert!(is_local_hostname("127.0.0.1"));
689 assert!(is_local_hostname("::1"));
690 assert!(is_local_hostname("192.168.0.1"));
691
692 assert!(!is_local_hostname("www.example.com"));
693 }
694
695 #[test]
696 fn is_ip_address() {
697 fn ip(s: &str) -> bool {
698 TorAddr::from(s).unwrap().is_ip_address()
699 }
700
701 assert!(ip("192.168.0.1:80"));
702 assert!(ip("[::1]:80"));
703 assert!(ip("[2001:db8::42]:65535"));
704 assert!(!ip("example.com:80"));
705 assert!(!ip("example.onion:80"));
706 }
707
708 #[test]
709 fn stream_instructions() {
710 use StreamInstructions as SI;
711
712 fn sap(s: &str) -> Result<StreamInstructions, ErrorDetail> {
713 TorAddr::from(s)
714 .unwrap()
715 .into_stream_instructions(&Default::default(), &mk_stream_prefs())
716 }
717
718 assert_eq!(
719 sap("[2001:db8::42]:9001").unwrap(),
720 SI::Exit {
721 hostname: "2001:db8::42".to_owned(),
722 port: 9001
723 },
724 );
725 assert_eq!(
726 sap("example.com:80").unwrap(),
727 SI::Exit {
728 hostname: "example.com".to_owned(),
729 port: 80
730 },
731 );
732
733 {
734 let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
735 let onion = format!("sss1234.www.{}.onion", b32);
736 let got = sap(&format!("{}:443", onion));
737
738 #[cfg(feature = "onion-service-client")]
739 assert_eq!(
740 got.unwrap(),
741 SI::Hs {
742 hsid: format!("{}.onion", b32).parse().unwrap(),
743 hostname: onion,
744 port: 443,
745 }
746 );
747
748 #[cfg(not(feature = "onion-service-client"))]
749 assert!(matches!(got, Err(ErrorDetail::OnionAddressNotSupported)));
750 }
751 }
752
753 #[test]
754 fn resolve_instructions() {
755 use ResolveInstructions as RI;
756
757 fn sap(s: &str) -> Result<ResolveInstructions, ErrorDetail> {
758 TorAddr::from(s)
759 .unwrap()
760 .into_resolve_instructions(&Default::default(), &Default::default())
761 }
762
763 assert_eq!(
764 sap("[2001:db8::42]:9001").unwrap(),
765 RI::Return(vec!["2001:db8::42".parse().unwrap()]),
766 );
767 assert_eq!(
768 sap("example.com:80").unwrap(),
769 RI::Exit("example.com".to_owned()),
770 );
771 assert!(matches!(
772 sap("example.onion:80"),
773 Err(ErrorDetail::OnionAddressResolveRequest),
774 ));
775 }
776
777 #[test]
778 fn bad_ports() {
779 assert_eq!(
780 TorAddr::from("www.example.com:squirrel"),
781 Err(TorAddrError::BadPort)
782 );
783 assert_eq!(
784 TorAddr::from("www.example.com:0"),
785 Err(TorAddrError::BadPort)
786 );
787 }
788
789 #[test]
790 fn prefs_onion_services() {
791 use crate::err::ErrorDetailDiscriminants;
792 use tor_error::{ErrorKind, HasKind as _};
793 use ErrorDetailDiscriminants as EDD;
794 use ErrorKind as EK;
795
796 #[allow(clippy::redundant_closure)] let prefs_def = || StreamPrefs::default();
798
799 let addr: TorAddr = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:443"
800 .parse()
801 .unwrap();
802
803 fn map(
804 got: Result<impl Sized, ErrorDetail>,
805 ) -> Result<(), (ErrorDetailDiscriminants, ErrorKind)> {
806 got.map(|_| ())
807 .map_err(|e| (ErrorDetailDiscriminants::from(&e), e.kind()))
808 }
809
810 let check_stream = |prefs, expected| {
811 let got = addr
812 .clone()
813 .into_stream_instructions(&Default::default(), &prefs);
814 assert_eq!(map(got), expected, "{prefs:?}");
815 };
816 let check_resolve = |prefs| {
817 let got = addr
818 .clone()
819 .into_resolve_instructions(&Default::default(), &prefs);
820 let expected = Err((EDD::OnionAddressResolveRequest, EK::NotImplemented));
823 assert_eq!(map(got), expected, "{prefs:?}");
824 };
825
826 cfg_if::cfg_if! {
827 if #[cfg(feature = "onion-service-client")] {
828 use tor_config::BoolOrAuto as B;
829 let prefs_of = |yn| {
830 let mut prefs = StreamPrefs::default();
831 prefs.connect_to_onion_services(yn);
832 prefs
833 };
834 check_stream(prefs_def(), Ok(()));
835 check_stream(prefs_of(B::Auto), Ok(()));
836 check_stream(prefs_of(B::Explicit(true)), Ok(()));
837 check_stream(prefs_of(B::Explicit(false)), Err((EDD::OnionAddressDisabled, EK::ForbiddenStreamTarget)));
838
839 check_resolve(prefs_def());
840 check_resolve(prefs_of(B::Auto));
841 check_resolve(prefs_of(B::Explicit(true)));
842 check_resolve(prefs_of(B::Explicit(false)));
843 } else {
844 check_stream(prefs_def(), Err((EDD::OnionAddressNotSupported, EK::FeatureDisabled)));
845
846 check_resolve(prefs_def());
847 }
848 }
849 }
850
851 #[test]
852 fn convert_safe() {
853 fn check<A: IntoTorAddr>(a: A, s: &str) {
854 let a1 = TorAddr::from(a).unwrap();
855 let a2 = s.parse().unwrap();
856 assert_eq!(a1, a2);
857 assert_eq!(&a1.to_string(), s);
858 }
859
860 check(("www.example.com", 8000), "www.example.com:8000");
861 check(
862 TorAddr::from(("www.example.com", 8000)).unwrap(),
863 "www.example.com:8000",
864 );
865 check(
866 TorAddr::from(("www.example.com", 8000)).unwrap(),
867 "www.example.com:8000",
868 );
869 let addr = "[2001:db8::0042]:9001".to_owned();
870 check(&addr, "[2001:db8::42]:9001");
871 check(addr, "[2001:db8::42]:9001");
872 check(("2001:db8::0042".to_owned(), 9001), "[2001:db8::42]:9001");
873 check(("example.onion", 80), "example.onion:80");
874 }
875
876 #[test]
877 fn convert_dangerous() {
878 fn check<A: DangerouslyIntoTorAddr>(a: A, s: &str) {
879 let a1 = TorAddr::dangerously_from(a).unwrap();
880 let a2 = TorAddr::from(s).unwrap();
881 assert_eq!(a1, a2);
882 assert_eq!(&a1.to_string(), s);
883 }
884
885 let ip: IpAddr = "203.0.133.6".parse().unwrap();
886 let ip4: Ipv4Addr = "203.0.133.7".parse().unwrap();
887 let ip6: Ipv6Addr = "2001:db8::42".parse().unwrap();
888 let sa: SocketAddr = "203.0.133.8:80".parse().unwrap();
889 let sa4: SocketAddrV4 = "203.0.133.8:81".parse().unwrap();
890 let sa6: SocketAddrV6 = "[2001:db8::43]:82".parse().unwrap();
891
892 #[allow(clippy::needless_borrow)]
894 #[allow(clippy::needless_borrows_for_generic_args)]
895 check(&(ip, 443), "203.0.133.6:443");
896 check((ip, 443), "203.0.133.6:443");
897 check((ip4, 444), "203.0.133.7:444");
898 check((ip6, 445), "[2001:db8::42]:445");
899 check(sa, "203.0.133.8:80");
900 check(sa4, "203.0.133.8:81");
901 check(sa6, "[2001:db8::43]:82");
902 }
903}