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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! # gsbrs
//!
//! gsbrs provides a GSBClient struct that can be used to query the
//! [Google Safe Browsing Lookup API](https://developers.google.com/safe-browsing/lookup_guide)
#![deny(warnings,missing_docs)]

extern crate hyper;
extern crate url;
#[cfg(all(test))]
extern crate quickcheck;

/// GSBError type for wrapping incorrect/ invalid behaviors
pub mod gsberror;

use gsberror::GSBError;
use hyper::Client;
use hyper::status::StatusCode;
use std::io::prelude::*;
use url::Url;

#[allow(non_upper_case_globals)]
/// Indicates the maximum number of urls Google can process at a time
pub static url_limit: u32 = 500;

/// Status represents each list a URL may be found in as well as a value,
/// The lookup_all API will return 'Ok' when the url is not found in any list, to maintain ordering.
#[derive(Debug, Clone, PartialEq)]
pub enum Status {
    /// URL was not found in bulk lookup
    Ok,
    /// Url was found in Phishing list
    Phishing,
    /// Url was found in Malware list
    Malware,
    /// Url was found in Unwanted list
    Unwanted,
}

/// A client for interacting with the Google Safe Browsing Lookup API
pub struct GSBClient<'b> {
    api_key: String,
    client_name: &'b str,
    app_ver: String,
    pver: String,
    client: hyper::client::Client,
}

impl<'b> GSBClient<'b> {
    /// Creates a new GSBClient that will use 'key' as the GSB API key
    ///
    ///
    /// # Arguments
    ///
    /// * `key` - A String that represents the API key for Google Safe Browsing Lookup
    ///
    /// # Example
    /// ```
    /// use gsbrs::GSBClient;
    /// let key : String = "example_api_key".into();
    /// let gsb = GSBClient::new(key);
    /// ```
    pub fn new(key: String) -> GSBClient<'b> {
        GSBClient {
            api_key: key,
            client_name: "gsbrs",
            app_ver: env!("CARGO_PKG_VERSION").to_owned(),
            pver: "3.1".to_owned(),
            client: Client::new(),
        }
    }

    /// Sets the GSBClient client_name to 'client_name'
    /// GSBClient uses 'gsbrs' as the client_name by default.
    ///
    /// # Arguments
    ///
    /// * `client_name` - A string slice that holds the name of the org/person using the client
    ///
    /// # Example
    /// ```
    /// let key : String = "example_api_key".into();
    /// let mut gsb = gsbrs::GSBClient::new(key);
    ///
    /// gsb.change_client_name("new_name");
    /// ```
    ///
    pub fn change_client_name(&mut self, client_name: &'b str) {
        self.client_name = client_name;
    }


    /// Returns a mutable reference to the hyper::Client used for connections    
    pub fn get_client_mut(&mut self) -> &mut hyper::Client {
        &mut self.client
    }
    /// Queries GSB API with 'url', returns Vec of Status for 'url'
    ///
    /// # Arguments
    ///
    /// * `url` - A string slice that holds the url to be queried
    ///
    /// # Example
    /// ```norun
    /// let key : String = "example_api_key".into();
    /// let gsb = gsbrs::GSBClient::new(key);
    ///
    /// let url = "https://google.com/";
    /// if let Ok(statuses) = gsb.lookup(url) {
    ///     assert_eq!(statuses[0], Status::Ok)
    /// };
    /// ```
    pub fn lookup(&self, url: &str) -> Result<Vec<Status>, GSBError> {
        let query = self.build_get_url(url);

        let msg = {
            let mut s = String::new();
            let mut res = try!((&self).client.get(&query).send());
            try!((&self).check_res(&mut res));
            try!(res.read_to_string(&mut s));
            s
        };

        let msg: Vec<&str> = msg.split(",").collect();

        let statuses = try!(self.statuses_from_vec(&msg));
        Ok(statuses)
    }

    /// Build a queryable String with 'url'
    fn build_get_url(&self, url: &str) -> String {
        let mut base = Url::parse("https://sb-ssl.google.com/safebrowsing/api/lookup?").unwrap();

        let v: Vec<(&str, &str)> = vec![("client", self.client_name.as_ref()),
                                        ("key", self.api_key.as_ref()),
                                        ("appver", self.app_ver.as_ref()),
                                        ("pver", self.pver.as_ref()),
                                        ("url", url)];

        base.set_query_from_pairs(v.into_iter());

        format!("{}", base)
    }

    /// Takes an iterator of &str and returns a single string and the number of items
    /// counted in the iterator. If there are > url_limit items, return GSBError::TooManyUrls
    fn url_list_from_iter<'a, I>(&self, urls: I) -> Result<(String, usize), GSBError>
        where I: Iterator<Item = &'a str>
    {
        let mut url_list = String::new();
        let mut length: usize = 0;

        for url in urls {
            length = length + 1;
            url_list.push_str(url);
            url_list.push('\n');
        }
        url_list.pop();
        let length = length;

        // GSB API only accepts 500 or fewer urls
        if length > url_limit as usize {
            return Err(GSBError::TooManyUrls);
        }

        Ok((url_list, length))
    }

    /// Returns GSBError::HTTPStatusCode if Response StatusCode is not 200
    fn check_res(&self, res: &mut hyper::client::response::Response) -> Result<(), GSBError> {

        if res.status != StatusCode::Ok {
            if res.status != StatusCode::NoContent {
                return Err(GSBError::HTTPStatusCode(res.status));
            }
        }
        Ok(())
    }

    /// Perform a bulk lookup on an iterable of urls.
    /// Returns a Vector of Vectors containing Statuses.
    /// Returns GSBError::TooManyUrls if > 500 urls are pased in
    ///
    /// # Arguments
    ///
    /// * `urls` - An iterable of string slices representing URLs to query
    ///
    /// # Example
    /// ```norun
    /// let key : String = "example_api_key".into();
    /// let gsb = GSBClient::new(key);
    ///
    /// let urls = vec!["https://google.com/", "https://example.com"];
    /// gsb.lookup_all(urls.iter());
    /// ```
    pub fn lookup_all<'a, I>(&self, urls: I) -> Result<Vec<Vec<Status>>, GSBError>
        where I: Iterator<Item = &'a str>
    {
        let url = self.build_post_url();

        let message = {
            let (url_list, length) = match (&self).url_list_from_iter(urls) {
                Ok((u, l)) => (u, l.to_string()),
                Err(e) => return Err(e),
            };
            // length of message is the length of url_li
            let mut message = String::with_capacity(length.len() + url_list.len());

            message.push_str(&length);
            message.push('\n');
            message.push_str(&url_list);
            message
        };

        let post = (&self).client.post(&url).body(&message);
        let mut res = try!(post.send());
        try!((&self).check_res(&mut res));
        let res = res;
        let msgs = try!(self.messages_from_response_post(res));

        Ok(msgs)
    }

    /// Takes a reponse from GSB and splits it into lines of Statuses
    fn messages_from_response_post<R: Read>(&self,
                                            mut res: R)
                                            -> Result<Vec<Vec<Status>>, GSBError> {
        let msgs = {
            let mut s = String::new();
            try!(res.read_to_string(&mut s));
            s
        };

        let msgs: Vec<&str> = msgs.split("\n").collect();
        let mut all_statuses = Vec::with_capacity(msgs.len());

        for status_line in msgs {
            let status_line: Vec<&str> = status_line.split(",").collect();
            let statuses = try!(self.statuses_from_vec(&status_line));
            if !statuses.is_empty() {
                all_statuses.push(statuses);
            }
        }

        Ok(all_statuses)
    }

    /// Builds a Vec<Status> from a slice of &str
    fn statuses_from_vec(&self, strstatuses: &[&str]) -> Result<Vec<Status>, GSBError> {
        let mut statuses = Vec::new();
        for status in strstatuses {
            let status = *status;
            match status {
                "phishing" => statuses.push(Status::Phishing),
                "malware" => statuses.push(Status::Malware),
                "unwanted" => statuses.push(Status::Unwanted),
                "ok" => statuses.push(Status::Ok),
                "" => (),
                _ => return Err(GSBError::MalformedMessage(status.to_owned())),
            }
        }
        Ok(statuses)
    }

    /// Builds a queryable string for POST requests
    fn build_post_url(&self) -> String {
        let mut base = Url::parse("https://sb-ssl.google.com/safebrowsing/api/lookup?").unwrap();

        let v: Vec<(&str, &str)> = vec![("client", self.client_name.as_ref()),
                                        ("key", self.api_key.as_ref()),
                                        ("appver", self.app_ver.as_ref()),
                                        ("pver", self.pver.as_ref())];

        base.set_query_from_pairs(v.into_iter());

        format!("{}", base)
    }

// pub fn canonicalize(url: Url) -> Url {
//     unimplemented!()
// }

}

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

    #[test]
    fn test_build_post_url() {
        let g = GSBClient::new("testkey".to_owned());

        let s = format!("https://sb-ssl.google.\
                         com/safebrowsing/api/lookup?client=gsbrs&key=testkey&appver={}&pver=3.1",
                        env!("CARGO_PKG_VERSION"));
        assert_eq!(g.build_post_url(), s.to_owned());
    }


    #[test]
    fn test_build_get_url() {
        let g = GSBClient::new("testkey".to_owned());
        let u = "https://google.com/".to_owned();
        let s = format!("https://sb-ssl.google.\
                         com/safebrowsing/api/lookup?client=gsbrs&key=testkey&appver={}&pver=3.\
                         1&url=https%3A%2F%2Fgoogle.com%2F",
                        env!("CARGO_PKG_VERSION"));
        assert_eq!(g.build_get_url(&u), s.to_owned());
    }

    #[test]
    fn test_statuses_from_vec() {
        let g = GSBClient::new("testkey".to_owned());
        let statuses = vec!["phishing", "malware", "unwanted", "ok"];
        let statuses = g.statuses_from_vec(&statuses).ok().expect("");
        assert_eq!(vec![Status::Phishing, Status::Malware, Status::Unwanted, Status::Ok],
                   statuses);

        let statuses = vec!["", "", "", ""];
        let statuses = g.statuses_from_vec(&statuses).ok().expect("");
        assert!(statuses.is_empty());

        let statuses = vec!["malformed"];
        let statuses = g.statuses_from_vec(&statuses).unwrap_err();
        match statuses {
            gsberror::GSBError::MalformedMessage(msg) => {
                assert_eq!(msg, "malformed");
            }
            _ => panic!(),
        }


    }
}

#[cfg(all(test))]
mod quicktests {
    use super::*;

    use quickcheck::quickcheck;

    fn quickcheck_build_get_url(url: String) {
        let g = GSBClient::new("testkey".to_owned());
        g.build_get_url(&url);
    }



    fn quickcheck_client_new(key: String) {
        let _ = GSBClient::new(key);
    }

    fn test_statuses_from_vec(strstatuses: Vec<String>) {
        let g = GSBClient::new("testkey".to_owned());
        let strstatuses: Vec<&str> = strstatuses.iter().map(|s| s.as_ref()).collect();
        let _ = g.statuses_from_vec(&strstatuses);
    }


    fn quickcheck_messages_from_response_post(cursor: String) {
        let g = GSBClient::new("testkey".to_owned());
        let cursor = cursor.as_bytes();
        let _ = g.messages_from_response_post(cursor);
    }

    fn quickcheck_set_name(name: String) {
        let mut g = GSBClient::new("testkey".to_owned());
        g.change_client_name(&name);
    }

    #[test]
    fn test() {
        quickcheck(quickcheck_set_name as fn(String));

        quickcheck(quickcheck_messages_from_response_post as fn(String));
        quickcheck(test_statuses_from_vec as fn(Vec<String>));

        quickcheck(quickcheck_build_get_url as fn(String));
        quickcheck(quickcheck_client_new as fn(String));
    }

}
//
// #[cfg(test)]
// mod bench {
//     use super::*;
//     extern crate test;
//     use self::test::Bencher;
//
//     #[bench]
//     fn bench_build_get_url(b: &mut Bencher) {
//         let gsb = GSBClient::new("test".to_owned());
//         b.iter(|| {
//             gsb.build_get_url("https://google.com/");
//         });
//     }
//
//     #[bench]
//     fn bench_build_post_url(b: &mut Bencher) {
//         let gsb = GSBClient::new("test".to_owned());
//         b.iter(|| {
//             gsb.build_post_url();
//         });
//     }
//
//     #[bench]
//     fn bench_lookup(b: &mut Bencher) {
//         let count = test::black_box(1000);
//         let mut bstatuses = Vec::with_capacity(count );
//         for _ in 0..count {
//             bstatuses.push(test::black_box(Status::Phishing));
//         }
//
//         b.iter(|| {
//             let key: String = "AIzaSyCOZpyGR3gMKqrb5A9lGSsVKtr7".into();
//             let gsb = GSBClient::new(key);
//             let statuses = match gsb.lookup("https://google.com") {
//                 _  => bstatuses.clone()
//             };
//
//             if statuses.is_empty() {
//                 println!("Ok");
//             } else {
//                 for status in statuses {
//                     match status {
//                         Status::Phishing => test::black_box(()),
//                         Status::Malware => test::black_box(()),
//                         Status::Unwanted => test::black_box(()),
//                         // lookup only ever returns the above 3 statuses
//                         _ => unreachable!(),
//                     }
//                 }
//             }
//         });
//     }
//
// }