oauth_api/
lib.rs

1#![cfg_attr(test, deny(warnings))]
2//! A library for making oauth2 requests with updated depencies like curl 0.3.0
3//!
4//! # Examples
5//!
6//! ```rust,no_run,ignore
7//! extern crate rustc_serialize;
8//! extern crate oauth-api;
9//!
10//! use rustc_serialize::json;
11//! use std::fs::File;
12//! use std::io::Read;
13//! /* Secrets.json sample contents:
14//! {
15//!   "client_id": "abcde",
16//!   "client_secret": "efgab",
17//!   "auth_url": "https://github.com/login/oauth/authorize",
18//!   "token_url": "https://github.com/login/oauth/access_token"
19//! }
20//! */
21//! let mut f = File::open("secrets.json").unwrap();
22//! let mut read_str = String::new();
23//! let _ = f.read_to_string(&mut read_str);
24//! let sec : Secret = json::decode(&read_str).unwrap();
25//!
26//! let mut conf = oauth2::Config::new(
27//!     &sec.client_id,
28//!     &sec.client_secret,
29//!     &sec.auth_url,
30//!     &sec.token_url
31//! );
32//! conf.scopes = vec!["repo".to_owned()];
33//! let url = conf.authorize_url("v0.0.1 gitbot".to_owned());
34//! println!("please visit this url: {}", url);
35//!
36//! let mut user_code = String::new();
37//! let _ = std::io::stdin().read_line(&mut user_code).unwrap();
38//! user_code.pop();
39//! let tok = conf.exchange(user_code).unwrap();
40//! println!("access code is: {}", tok.access_token);
41//! ```
42//!
43
44extern crate url;
45extern crate curl;
46#[macro_use] extern crate log;
47
48use url::Url;
49use std::sync::{Arc,Mutex};
50use std::io::Read;
51
52use curl::easy::{Easy, List};
53
54/// Configuration of an oauth2 application.
55pub struct Config {
56    pub client_id: String,
57    pub client_secret: String,
58    pub scopes: Vec<String>,
59    pub auth_url: Url,
60    pub token_url: Url,
61    pub redirect_url: String,
62}
63
64/// Represents a Token struct
65#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
66pub struct Token {
67    /// access token used to authenticate queries
68    pub access_token: String,
69    /// A vec of scopes
70    pub scopes: Vec<String>,
71    /// 'bearer', etc...
72    pub token_type: String,
73}
74
75struct ErrorContainer {
76    error : String,
77    error_desc : String,
78    error_uri : String
79}
80
81impl ErrorContainer {
82    fn new() -> ErrorContainer {
83        ErrorContainer {
84            error: String::new(),
85            error_desc: String::new(),
86            error_uri: String::new()
87        }
88    }
89}
90
91macro_rules! try_error_to_string {
92    ($e:expr) => (match $e {
93        Ok(val) => val,
94        Err(err) => return Err(::std::convert::From::from(error_to_string(err))),
95    });
96}
97
98
99/// Helper trait for extending the builder-style pattern of curl::easy::Easy.
100///
101/// This trait allows chaining the correct authorization headers onto a curl
102/// request via the builder style.
103pub trait Authorization {
104    fn auth_with(&mut self, token: &Token) -> Result<(), curl::Error>;
105}
106
107impl Config {
108
109    /// Generates a new config from the given fields
110    pub fn new(id: &str, secret: &str, auth_url: &str,
111               token_url: &str) -> Config {
112        Config {
113            client_id: id.to_string(),
114            client_secret: secret.to_string(),
115            scopes: Vec::new(),
116            auth_url: Url::parse(auth_url).unwrap(),
117            token_url: Url::parse(token_url).unwrap(),
118            redirect_url: String::new(),
119        }
120    }
121
122    #[allow(deprecated)] // connect => join in 1.3
123    /// Generates an auth url to visit from the infomation in the config struct
124    pub fn authorize_url(&self, state: String) -> Url {
125        let scopes = self.scopes.connect(",");
126        let mut pairs = vec![
127            ("client_id", &self.client_id),
128            ("state", &state),
129            ("scope", &scopes),
130        ];
131        if self.redirect_url.len() > 0 {
132            pairs.push(("redirect_uri", &self.redirect_url));
133        }
134        let mut url = self.auth_url.clone();
135
136        for (k,v) in pairs {
137            url.query_pairs_mut().append_pair(k,v);
138        }
139        return url;
140    }
141
142    /// Given a code (obtained from the authorize_url) and varies by service.
143    /// exchange will then make a POST request with the code and attempt to retrieve an access token.
144    /// On success, the token is returned as a Result. On failure, a string with an error description
145    /// is returned as a Result
146    pub fn exchange(&self, code: String) -> Result<Token, String> {
147        let mut form = url::form_urlencoded::Serializer::new(String::new());
148        form.append_pair("client_id", &self.client_id.clone());
149        form.append_pair("client_secret", &self.client_secret.clone());
150        form.append_pair("code", &code);
151        if self.redirect_url.len() > 0 {
152            form.append_pair("redirect_uri", &self.redirect_url.clone());
153        }
154
155        let form_str : String = form.finish();
156        let post_len = form_str.as_bytes().len();
157
158        let mut easy = Easy::new();
159        try_error_to_string!(easy.url(&self.token_url.to_string()));
160        let mut list = List::new();
161        try_error_to_string!(list.append("Content-Type: application/x-www-form-urlencoded"));
162        try_error_to_string!(easy.http_headers(list));
163        try_error_to_string!(easy.show_header(true));
164        try_error_to_string!(easy.read_function(move |buf| {
165            Ok(form_str.as_bytes().read(buf).unwrap_or(0))
166        }));
167        try_error_to_string!(easy.post(true));
168        try_error_to_string!(easy.post_field_size(post_len as u64));
169
170        let token = Token {
171            access_token: String::new(),
172            scopes: Vec::new(),
173            token_type: String::new(),
174        };
175
176        let protector = Arc::new(Mutex::new(token));
177        let result_ref = protector.clone();
178        let error_strings = Arc::new(Mutex::new(ErrorContainer::new()));
179        let error_strings_copy = error_strings.clone();
180
181        try_error_to_string!(easy.write_function(move |data| {
182            let mut result_token = result_ref.lock().unwrap();
183            let mut err_cont = error_strings_copy.lock().unwrap();
184
185            let result_form = url::form_urlencoded::parse(data);
186            for(k, v) in result_form.into_iter() {
187                match &k[..] {
188                    "access_token" => result_token.access_token = (*v).to_owned(),
189                    "token_type" => result_token.token_type = (*v).to_owned(),
190                    "scope" => {
191                        result_token.scopes = v.split(',')
192                                        .map(|s| s.to_string()).collect();
193                    },
194                     "error" => err_cont.error = (*v).to_owned(),
195                     "error_description" => err_cont.error_desc = (*v).to_owned(),
196                     "error_uri" => err_cont.error_uri = (*v).to_owned(),
197                    _ => {}
198                }
199            }
200            return Ok(data.len());
201        }));
202
203        try_error_to_string!(easy.perform());
204
205        let resp_code = try_error_to_string!(easy.response_code());
206        if resp_code != 200 {
207            return Err(format!("expected `200`, found `{}`", resp_code))
208        }
209
210        let new_token = protector.lock().unwrap();
211        let new_errors = error_strings.lock().unwrap();
212
213        if new_token.access_token.len() != 0 {
214            Ok(new_token.clone())
215        } else if new_errors.error.len() > 0 {
216            Err(format!("error `{}`: {}, see {}", new_errors.error, new_errors.error_desc, new_errors.error_uri))
217        } else {
218            Err(format!("couldn't find access_token in the response"))
219        }
220    }
221}
222
223fn error_to_string(e : curl::Error) -> String {
224    let err_str : &str;
225    err_str = if e.is_unsupported_protocol() {
226        "Unsupported Protocol!"
227    } else if e.is_failed_init() {
228        "Failed to initialize"
229    } else if e.is_url_malformed() {
230        "Url is malformed!"
231    } else if e.is_couldnt_resolve_proxy() {
232        "Couldn't resolve proxy"
233    } else if e.is_couldnt_resolve_host() {
234        "Couldn't Resolve host"
235    } else if e.is_couldnt_connect() {
236        "Couldn't Connect"
237    } else if e.is_remote_access_denied() {
238        "Remote access is denied"
239    } else if e.is_partial_file() {
240        "Partial file given"
241    } else if e.is_quote_error() {
242        "Quote error"
243    } else if e.is_http_returned_error() {
244        "Http returned error"
245    } else if e.is_read_error() {
246        "Read error"
247    } else if e.is_write_error() {
248        "Write Error"
249    } else if e.is_upload_failed() {
250        "Upload failed"
251    } else if e.is_out_of_memory() {
252        "Out of memory"
253    } else if e.is_operation_timedout() {
254        "Timed out"
255    } else if e.is_range_error() {
256        "Range error"
257    } else if e.is_http_post_error() {
258        "Http post error"
259    } else if e.is_ssl_connect_error() {
260        "SSL connect error"
261    } else if e.is_bad_download_resume() {
262        "Bad download resume error"
263    } else if e.is_file_couldnt_read_file() {
264        "Cannot read given file"
265    } else if e.is_function_not_found() {
266        "Cannot find given function error"
267    } else if e.is_aborted_by_callback() {
268        "Callback aborted error"
269    } else if e.is_bad_function_argument() {
270        "Bad function argument error"
271    } else if e.is_interface_failed() {
272        "Interface failed error"
273    } else if e.is_too_many_redirects() {
274        "Too many redirects error"
275    } else if e.is_unknown_option() {
276        "Unknown option error"
277    } else if e.is_peer_failed_verification() {
278        "Peer failed to validate error"
279    } else if e.is_got_nothing() {
280        "Received nothing error"
281    } else if e.is_ssl_engine_notfound() {
282        "SSL engine not found error"
283    } else if e.is_ssl_engine_setfailed() {
284        "SSL engine set failed error"
285    } else if e.is_send_error() {
286        "Send failed error"
287    } else if e.is_recv_error() {
288        "Recieve failed error"
289    } else if e.is_ssl_certproblem() {
290        "SSL certificate problem error"
291    } else if e.is_ssl_cipher() {
292        "SSL cipher error"
293    } else if e.is_ssl_cacert() {
294        "SSL CA Cert error"
295    } else if e.is_bad_content_encoding() {
296        "Bad content encoding error"
297    } else if e.is_filesize_exceeded() {
298        "Filesize exceeded error"
299    } else if e.is_use_ssl_failed() {
300        "Use SSL failed error"
301    } else if e.is_send_fail_rewind() {
302        "Send rewind fail error"
303    } else if e.is_ssl_engine_initfailed() {
304        "SSL engine init fail error"
305    } else if e.is_login_denied() {
306        "Login denied error"
307    } else if e.is_conv_failed() {
308        "Conv failed error"
309    } else if e.is_conv_required() {
310        "Conv required error"
311    } else if e.is_ssl_cacert_badfile() {
312        "CA cert bad file error"
313    } else if e.is_ssl_crl_badfile() {
314        "SSL crl bad file error"
315    } else if e.is_ssl_shutdown_failed() {
316        "SSL Shutdown failed error"
317    } else if e.is_again() {
318        "Again error"
319    } else if e.is_ssl_issuer_error() {
320        "SSL Issuer error"
321    } else if e.is_chunk_failed() {
322        "Chunk failed error"
323    } else {
324        "general error"
325    };
326    return err_str.to_string();
327}
328
329/// Given a curl::easy::Easy and a `Token` struct, it adds the Authorization: access_token header
330/// to the request. It return curl::Error when adding the header fails.
331impl Authorization for curl::easy::Easy{
332    fn auth_with(&mut self, token: &Token) -> Result<(), curl::Error> {
333        let mut auth_header = List::new();
334        let auth_header_text = format!("Authorization: {}", token.access_token);
335        let res = auth_header.append(&auth_header_text);
336        if res.is_ok() {
337            self.http_headers(auth_header)
338        } else {
339            res
340        }
341    }
342}