tor_check/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use std::{error, fmt};
4#[cfg(feature = "reqwest")]
5use std::{future::Future, pin::Pin};
6
7use serde::Deserialize;
8
9/// `Result` alias for blocking HTTP client.
10type Result<T, E> = std::result::Result<T, TorCheckError<E>>;
11/// `Result` alias for asynchronous HTTP client.
12#[cfg(feature = "reqwest")]
13type FutureResult<T, E> = Pin<Box<dyn Future<Output = Result<T, E>>>>;
14
15/// Tor check response status.
16#[derive(Debug, Deserialize)]
17struct TorCheckStatus {
18    #[serde(rename = "IsTor")]
19    is_tor: bool,
20}
21
22impl TorCheckStatus {
23    const API_URL: &str = "https://check.torproject.org/api/ip";
24
25    fn result<E: error::Error>(&self) -> Result<(), E> {
26        if self.is_tor {
27            return Ok(());
28        }
29
30        Err(TorCheckError::YouAreNotUsingTor)
31    }
32}
33
34/// Trait for Tor connection checking.
35pub trait TorCheck {
36    type Result;
37
38    /// Verify if you are correctly connected to Tor.
39    ///
40    /// Return the HTTP client on success.
41    fn tor_check(self) -> Self::Result;
42}
43
44/// Potentials errors returned on the Tor verification process.
45///
46/// Where `E` is the HTTP client error type.
47#[derive(Debug, PartialEq)]
48pub enum TorCheckError<E> {
49    /// Error returned by the HTTP client.
50    HttpClient(E),
51    /// The check page indicate that you are not using Tor.
52    YouAreNotUsingTor,
53}
54
55#[cfg(feature = "reqwest")]
56impl TorCheckError<reqwest::Error> {
57    /// Returns true if the error is related to the JSON response
58    /// deserialization.
59    pub fn is_decode(&self) -> bool {
60        if let Self::HttpClient(err) = self {
61            return err.is_decode();
62        }
63
64        false
65    }
66}
67
68#[cfg(feature = "ureq")]
69impl TorCheckError<ureq::Error> {
70    /// Returns true if the error is related to the JSON response
71    /// deserialization.
72    pub fn is_decode(&self) -> bool {
73        matches!(self, Self::HttpClient(ureq::Error::Json(_)))
74    }
75}
76
77impl<E: error::Error> fmt::Display for TorCheckError<E> {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::HttpClient(err) => write!(f, "{err}"),
81            Self::YouAreNotUsingTor => write!(f, "You are not using Tor"),
82        }
83    }
84}
85
86impl<E: error::Error> error::Error for TorCheckError<E> {}
87
88#[cfg(feature = "ureq")]
89impl From<ureq::Error> for TorCheckError<ureq::Error> {
90    fn from(err: ureq::Error) -> Self {
91        Self::HttpClient(err)
92    }
93}
94
95#[cfg(feature = "reqwest")]
96impl From<reqwest::Error> for TorCheckError<reqwest::Error> {
97    fn from(err: reqwest::Error) -> Self {
98        Self::HttpClient(err)
99    }
100}
101
102#[cfg(feature = "ureq")]
103impl TorCheck for ureq::Agent {
104    type Result = Result<Self, ureq::Error>;
105
106    fn tor_check(self) -> Self::Result {
107        self.get(TorCheckStatus::API_URL)
108            .call()?
109            .body_mut()
110            .read_json::<TorCheckStatus>()?
111            .result()
112            .map(move |_| self)
113    }
114}
115
116#[cfg(feature = "reqwest")]
117impl TorCheck for reqwest::Client {
118    type Result = FutureResult<Self, reqwest::Error>;
119
120    fn tor_check(self) -> Self::Result {
121        Box::pin(async move {
122            self.get(TorCheckStatus::API_URL)
123                .send()
124                .await?
125                .json::<TorCheckStatus>()
126                .await?
127                .result()
128                .map(move |_| self)
129        })
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::{TorCheckError as Error, *};
136
137    #[derive(Debug, PartialEq)]
138    struct MockError;
139
140    impl fmt::Display for MockError {
141        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142            write!(f, "Mock error")
143        }
144    }
145
146    impl error::Error for MockError {}
147
148    #[test]
149    fn test_tor_check_result() {
150        let failure = serde_json::from_str::<TorCheckStatus>(
151            r#"{"IsTor":false,"IP":"192.0.2.1"}"#,
152        )
153        .unwrap();
154        let success = serde_json::from_str::<TorCheckStatus>(
155            r#"{"IsTor":true,"IP":"192.0.2.1"}"#,
156        )
157        .unwrap();
158        let expected_error = Error::YouAreNotUsingTor;
159
160        assert_eq!(failure.result::<MockError>(), Err(expected_error));
161        assert_eq!(success.result::<MockError>(), Ok(()));
162    }
163}