1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! The enrichment module exposes functionality to enrich
//! a given domain with interesting metadata. Currently
//! including:
//!
//! * DNS resolution (through HTTP/80 lookup).
//! * Open SMTP server (for email misdirects).
//!
//! Example:
//!
//! ```
//! use twistrs::enrich::DomainMetadata;
//!
//! let domain_metadata = DomainMetadata::new("google.com");
//! domain_metadata.dns_resolvable().await;
//! ```
//!
//! Note that the enrichment module is independent from the
//! permutation module and can be used with any given FQDN.

use std::fmt;
use std::net::IpAddr;

use tokio::net;

use async_smtp::{ClientSecurity, Envelope, SendableEmail, SmtpClient};

/// Temporary type-alias over `EnrichmentError`.
pub type Result<T> = std::result::Result<T, EnrichmentError>;

#[derive(Copy, Clone, Debug)]
#[deprecated(
    since = "0.1.0",
    note = "Prone to be removed in the future, does not currently provide any context."
)]
pub struct EnrichmentError;

impl fmt::Display for EnrichmentError {
    // @CLEANUP(jdb): Make this something meaningful, if it needs to be
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "")
    }
}

/// Container to store interesting FQDN metadata
/// on domains that we resolve.
///
/// Whenever any domain enrichment occurs, the
/// following struct is return to indicate the
/// information that was derived.
///
/// **N.B**—there will be cases where a single
/// domain can have multiple DomainMetadata
/// instancees associated with it.
#[derive(Debug, Clone)]
pub struct DomainMetadata {
    /// The domain that is being enriched.
    pub fqdn: String,

    /// Any IPv4 and IPv6 ips that were discovered during
    /// domain resolution.
    pub ips: Option<Vec<IpAddr>>,

    /// Any SMTP message data (if any) that was returned by
    /// an SMTP server.
    pub smtp: Option<SmtpMetadata>,
}

/// SMTP specific metadata generated by a partic
/// ular domain.
#[derive(Debug, Clone)]
pub struct SmtpMetadata {
    /// Whether the email was dispatched successfully
    is_positive: bool,

    /// Message received back from the SMTP server
    message: String,
}

impl DomainMetadata {
    /// Create a new empty state for a particular FQDN.
    pub fn new(fqdn: String) -> DomainMetadata {
        DomainMetadata {
            fqdn: fqdn,
            ips: None,
            smtp: None,
        }
    }

    /// Asynchronous DNS resolution on a DomainMetadata instance.
    ///
    /// Returns `Ok(DomainMetadata)` is the domain was resolved,
    /// otherwise returns `Err(EnrichmentError)`.
    ///
    /// **N.B**—also host lookups are done over port 80.
    pub async fn dns_resolvable(&self) -> Result<DomainMetadata> {
        match net::lookup_host(&format!("{}:80", self.fqdn)[..]).await {
            Ok(addrs) => Ok(DomainMetadata {
                fqdn: self.fqdn.clone(),
                ips: Some(addrs.map(|addr| addr.ip()).collect()),
                smtp: None,
            }),
            Err(_) => Err(EnrichmentError),
        }
    }

    /// Asynchronous SMTP check. Attempts to establish an SMTP
    /// connection to the FQDN on port 25 and send a pre-defi
    /// ned email.
    ///
    /// Currently returns `Ok(DomainMetadata)` always, which
    /// internally contains `Option<SmtpMetadata>`. To check
    /// if the SMTP relay worked, check that
    /// `DomainMetadata.smtp` is `Some(v)`.
    pub async fn mx_check(&self) -> Result<DomainMetadata> {
        let email = SendableEmail::new(
            Envelope::new(
                Some("twistrs@example.com".parse().unwrap()),
                vec!["twistrs@example.com".parse().unwrap()],
            )
            .unwrap(),
            "Twistrs",
            "And that's how the cookie crumbles\n",
        );

        let smtp_domain = format!("{}:25", &self.fqdn);

        match SmtpClient::with_security(smtp_domain.clone(), ClientSecurity::None).await {
            // TODO(jdb): Figure out how to clean this up
            Ok(smtp) => {
                match smtp.into_transport().connect_and_send(email).await {
                    Ok(response) => Ok(DomainMetadata {
                        fqdn: self.fqdn.clone(),
                        ips: None,
                        smtp: Some(SmtpMetadata {
                            is_positive: response.is_positive(),
                            message: response
                                .message
                                .into_iter()
                                .map(|s| s.to_string())
                                .collect::<String>(),
                        }),
                    }),

                    // @CLEANUP(JDB): Currently for most scenarios, the following call with return
                    //                an `std::io::ErrorKind::ConnectionRefused` which is normal.
                    //
                    //                In such a scenario, we still do not want to panic but instead
                    //                move on. Currently lettre::smtp::error::Error does not suppo-
                    //                rt the `fn kind` function to be able to handle error variant-
                    //                s. Try to figure out if there is another way to handle them.
                    Err(_) => Ok(DomainMetadata {
                        fqdn: self.fqdn.clone(),
                        ips: None,
                        smtp: None,
                    }),
                }
            }
            Err(_) => Ok(DomainMetadata {
                fqdn: self.fqdn.clone(),
                ips: None,
                smtp: None,
            }),
        }
    }

    /// Performs all FQDN enrichment methods on a given FQDN.
    /// This is the only function that returns a `Vec<DomainMetadata>`.
    ///
    /// # Panics
    ///
    /// Currently panics if any of the internal enrichment methods returns
    /// an Err.
    pub async fn all(&self) -> Result<Vec<DomainMetadata>> {
        // @CLEANUP(JDB): This should use try_join! in the future instead
        let result = futures::join!(self.dns_resolvable(), self.mx_check());

        Ok(vec![result.0.unwrap(), result.1.unwrap()])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::executor::block_on;

    #[tokio::test]
    async fn test_mx_check() {
        let domain_metadata = DomainMetadata::new(String::from("example.com"));
        assert!(block_on(domain_metadata.mx_check()).is_ok());
    }

    #[tokio::test]
    async fn test_all_modes() {
        let domain_metadata = DomainMetadata::new(String::from("example.com"));
        assert!(block_on(domain_metadata.all()).is_ok());
    }

    #[tokio::test]
    async fn test_dns_lookup() {
        let domain_metadata = DomainMetadata::new(String::from("example.com"));
        assert!(block_on(domain_metadata.dns_resolvable()).is_ok());
    }
}