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
//! Whenever you create a `RedditClient`, you need to provide an `Authenticator` that can
//! log in for you and send credentials with every request. Reddit's API provides multiple
//! ways to authenticate, and it's important that you use the correct one for your application
//! so that you only take passwords where necessary.
//! # OAuth Or Not?
//! In effect, Reddit's API is split into two parts: a deprecated API that uses cookies for
//! authentication, and an OAuth API that is recommended for new applications. Clients that use
//! OAuth-based authenticators have a higher rate limit (60/min for OAuth, 30/min for cookie), so
//! it may be preferable to use OAuth if larger batches of data are being processed from the API.
//! # Authenticator Summary
//! - `AnonymousAuthenticator` - uses the legacy API (so it has a lower rate limit) but requires
//! no credentials at all. Choose this if you just want to **browse the API without registering**.
//! - `PasswordAuthenticator` - uses the OAuth API (so higher rate limits), but requires a
//! registered account and registration on the 'apps' page (see below). Choose this for **bots**
//! or scripts that use lots of data.
//!
//! TODO: Add authenticators for the other flows and document them.
//!
//! # Registering Your App (for OAuth-based authenticators)
//! **Note: this does not apply to `AnonymousAuthenticator`**.
//!
//! In order to register to use OAuth, you need to login on your **bot account** and create an
//! 'app'. All you have to do is:
//!
//! 1. Go to your [app preferences](https://www.reddit.com/prefs/apps)
//! 2. Click 'create app' (or 'create another app', if you've already created one)
//! 3. Enter a name for your bot.
//! 4. Leave the description and about URL blank (unless you want to fill these in!)
//! 5. Choose the correct app type. If you want to use `PasswordAuthenticator`, choose **script**.
//! 6. Set the redirect URL to 'http://www.example.com/rawr' - this will not be used.
//! 7. Click 'create app'.
//!
//! You'll probably be able to see something like [this](http://bit.ly/29PR8XN) now. If so, you've
//! successfully created your app. Don't close it yet, because we need to get some 'secrets'
//! from that page.
//! # OAuth Secrets
//! In addition to your username and password, `PasswordAuthenticator` requires a client ID and
//! client secret token. The only way to get this is by registering an app. If you've followed
//! the steps above, you're already in the right place to follow the next steps.
//!
//! On [this](http://bit.ly/29PR8XN) page, the client ID is the random string below 'personal use
//! script' and the client secret is the underlined string. Store both of these somewhere safe,
//! and treat the client secret like a password - it **must** not be shared with anyone!
//!
//! You're now ready to create a `PasswordAuthenticator`. Ensure you provide all parameters in the
//! correct order:
//!
//! ```rust,ignore
//! # use rawr::auth::PasswordAuthenticator;
//! PasswordAuthenticator::new(CLIENT_ID, CLIENT_SECRET, USERNAME, PASSWORD);
//! ```

#![allow(unknown_lints, doc_markdown)]

use std::sync::{Arc, Mutex};
use hyper;
use hyper::header::{Headers, Authorization, Basic, Bearer, UserAgent};
use std::io::Read;
use serde_json;
use responses::auth::TokenResponse;
use hyper::client::Client;
use errors::APIError;

/// Trait for any method of authenticating with the Reddit API.
pub trait Authenticator {
    /// Logs in and fetches relevant tokens.
    fn login(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError>;
    /// Called if a token expiration error occurs.
    fn refresh_token(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError> {
        self.login(client, user_agent)
    }
    /// Logs out and invalidates tokens if applicable.
    fn logout(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError>;
    /// A list of OAuth scopes that this `Authenticator` can access. Currently, the result of this
    /// is not used, but the correct scopes should be returned. If all scopes can be accessed,
    /// this is signified by a vec!["*"]. If it is read-only, the result is vec!["read"].
    fn scopes(&self) -> Vec<String>;
    /// Returns the headers needed to authenticate. Must be done **after** `login()`.
    fn headers(&self) -> Headers;
    /// `true` if this authentication method requires the OAuth API.
    fn oauth(&self) -> bool;
}

/// An anonymous login authenticator.
pub struct AnonymousAuthenticator;

impl Authenticator for AnonymousAuthenticator {
    #[allow(unused_variables)]
    fn login(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError> {
        // Don't log in, because we're anonymous!
        Ok(())
    }

    #[allow(unused_variables)]
    fn logout(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError> {
        // Can't log out if we're not logged in.
        Ok(())
    }

    fn scopes(&self) -> Vec<String> {
        vec![String::from("read")]
    }

    fn headers(&self) -> Headers {
        Headers::new()
    }

    fn oauth(&self) -> bool {
        false
    }
}

impl AnonymousAuthenticator {
    /// Creates a new `AnonymousAuthenticator`. See the module-level documentation for the purpose
    /// of `AnonymousAuthenticator`.
    /// # Examples
    /// ```
    /// use rawr::auth::AnonymousAuthenticator;
    /// AnonymousAuthenticator::new();
    /// ```
    pub fn new() -> Arc<Mutex<Box<Authenticator + Send>>> {
        Arc::new(Mutex::new(Box::new(AnonymousAuthenticator {})))
    }
}

/// Authenticates using a username and password with OAuth. See the module-level documentation for
/// usage.
pub struct PasswordAuthenticator {
    access_token: Option<String>,
    client_id: String,
    client_secret: String,
    username: String,
    password: String,
}

impl Authenticator for PasswordAuthenticator {
    fn login(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError> {
        let url = "https://www.reddit.com/api/v1/access_token";
        let body = format!("grant_type=password&username={}&password={}",
                           &self.username,
                           &self.password);
        let access_req = client.post(url)
            .header(Authorization(Basic {
                username: self.client_id.to_owned(),
                password: Some(self.client_secret.to_owned()),
            }))
            .header(UserAgent(user_agent.to_owned()))
            .body(&body);

        let mut result = access_req.send().unwrap();

        if result.status != hyper::Ok {
            Err(APIError::HTTPError(result.status))
        } else {
            let mut buf = String::new();
            result.read_to_string(&mut buf).unwrap();
            let token_response: TokenResponse = serde_json::from_str(&buf).unwrap();
            self.access_token = Some(token_response.access_token);
            Ok(())
        }
    }

    fn logout(&mut self, client: &Client, user_agent: &str) -> Result<(), APIError> {
        let url = "https://www.reddit.com/api/v1/revoke_token";
        let body = format!("token={}", &self.access_token.to_owned().unwrap());
        let req = client.post(url)
            .header(Authorization(Basic {
                username: self.client_id.to_owned(),
                password: Some(self.client_secret.to_owned()),
            }))
            .header(UserAgent(user_agent.to_owned()))
            .body(&body);
        let res = req.send().unwrap();
        if !res.status.is_success() {
            Err(APIError::HTTPError(res.status))
        } else {
            Ok(())
        }
    }

    fn scopes(&self) -> Vec<String> {
        vec![String::from("*")]
    }

    fn headers(&self) -> Headers {
        let mut headers = Headers::new();
        if let Some(ref token) = self.access_token {
            headers.set(Authorization(Bearer { token: token.to_owned() }));
        }
        headers
    }

    fn oauth(&self) -> bool {
        true
    }
}

impl PasswordAuthenticator {
    /// Creates a new `PasswordAuthenticator`. If you do not have a client ID and secret (or do
    /// not know what these are), you need to fetch one using the instructions in the module
    /// documentation.
    pub fn new(client_id: &str,
               client_secret: &str,
               username: &str,
               password: &str)
               -> Arc<Mutex<Box<Authenticator + Send>>> {
        Arc::new(Mutex::new(Box::new(PasswordAuthenticator {
            client_id: client_id.to_owned(),
            client_secret: client_secret.to_owned(),
            username: username.to_owned(),
            password: password.to_owned(),
            access_token: None,
        })))
    }
}