├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples └── user_timeline.rs └── src ├── lib.rs └── tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.vscode 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | notifications: 4 | email: false 5 | 6 | script: 7 | - cargo build 8 | - cargo test 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oauthcli" 3 | version = "2.0.0-beta-2" 4 | authors = ["azyobuzin "] 5 | description = "Implementation of OAuth 1.0 (and Twitter's f*ckin' OAuth) Client" 6 | documentation = "https://docs.rs/oauthcli/2.0.0-beta-2/oauthcli/" 7 | repository = "https://github.com/azyobuzin/rust-oauthcli" 8 | readme = "README.md" 9 | keywords = ["oauth"] 10 | categories = ["authentication"] 11 | license = "MIT/Apache-2.0" 12 | 13 | [badges] 14 | travis-ci = { repository = "azyobuzin/rust-oauthcli" } 15 | 16 | [dependencies] 17 | base64 = "0.6" 18 | rand = "0.3" 19 | ring = { version = "0.12", default-features = false } 20 | time = "0.1" 21 | url = "1" 22 | 23 | [dev-dependencies] 24 | futures = "0.1.14" 25 | hyper = "0.11" 26 | hyper-tls = "0.1" 27 | tokio-core = "0.1.6" 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 azyobuzin and contributors 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 6 | * MIT license (http://opensource.org/licenses/MIT) 7 | 8 | at your option. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oauthcli 2 | ![crates.io](https://img.shields.io/crates/v/oauthcli.svg) 3 | 4 | Yet Another OAuth 1.0 Client Library for Rust 5 | 6 | # Features 7 | - RFC 5849 implementation (without RSA-SHA1) 8 | - Compatible with Twitter's (f*ckin') implementation 9 | 10 | # How to Use 11 | ```rust 12 | extern crate oauthcli; 13 | extern crate url; 14 | 15 | let header = 16 | oauthcli::OAuthAuthorizationHeaderBuilder::new( 17 | "POST", 18 | url::Url::parse("https://example").unwrap(), 19 | "Consumer Key", 20 | "Consumer Secret", 21 | oauthcli::SignatureMethod::HmacSha1 // or Plaintext 22 | ) 23 | .token("OAuth Token", "OAuth Token Secret") 24 | .request_parameters(vec![("status", "hello")].into_iter()) 25 | .finish(); 26 | 27 | assert_eq!(header.to_string(), "OAuth ......") 28 | ``` 29 | 30 | # Help me 31 | `oauthcli` has already reached v1.0.0 although `ring` is not stable. 32 | What shoud I do for not breaking the compatibility? 33 | -------------------------------------------------------------------------------- /examples/user_timeline.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | extern crate hyper; 3 | extern crate hyper_tls; 4 | extern crate oauthcli; 5 | extern crate tokio_core; 6 | 7 | use futures::{future, Future, Stream}; 8 | use hyper::header; 9 | use oauthcli::{OAuthAuthorizationHeaderBuilder, SignatureMethod}; 10 | use oauthcli::url::Url; 11 | 12 | fn main() { 13 | let req = { 14 | let url = Url::parse("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=azyobuzin").unwrap(); 15 | 16 | // @imgazyobuzi readonly token 17 | let auth_header = OAuthAuthorizationHeaderBuilder::new( 18 | "GET", &url, "uiYQy5R2RJFZRZ4zvSk7A", "qzDldacVrcyXbp8pBerf1LBfnQXmkPKmyLVGGLus8", SignatureMethod::HmacSha1) 19 | .token("862962650-rIcjsj0j9ZJ8khPVA8jZTtEJuq7YYDBDpx6fOAgb", "kbMQjdVldI6tFOST3SVjmyAtG1D0oCkCpL6vBv1FtA") 20 | .finish_for_twitter(); 21 | 22 | let mut req = hyper::Request::new(hyper::Get, url.as_str().parse().unwrap()); 23 | req.headers_mut().set(header::Authorization(auth_header.to_string())); 24 | req 25 | }; 26 | 27 | let mut core = tokio_core::reactor::Core::new().unwrap(); 28 | let handle = core.handle(); 29 | let client = hyper::Client::configure() 30 | .connector(hyper_tls::HttpsConnector::new(1, &handle).unwrap()) 31 | .build(&handle); 32 | 33 | let f = client.request(req) 34 | .and_then(|res| { 35 | let buf = match res.headers().get::() { 36 | Some(&header::ContentLength(x)) => Vec::with_capacity(x as usize), 37 | None => Vec::new(), 38 | }; 39 | 40 | res.body().fold(buf, |mut buf, chunk| { 41 | buf.extend(chunk); 42 | future::ok::<_, hyper::Error>(buf) 43 | }) 44 | }) 45 | .and_then(|buf| 46 | String::from_utf8(buf) 47 | .map_err(|e| hyper::Error::Utf8(e.utf8_error())) 48 | ); 49 | 50 | let res = core.run(f).unwrap(); 51 | println!("{}", res); 52 | } 53 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Yet Another OAuth 1.0 Client Library for Rust 2 | //! 3 | //! # Examples 4 | //! Basic sample: 5 | //! 6 | //! ``` 7 | //! # extern crate url; 8 | //! # extern crate oauthcli; 9 | //! use oauthcli::*; 10 | //! # fn main() { 11 | //! let url = url::Url::parse("http://example.com/").unwrap(); 12 | //! let header = 13 | //! OAuthAuthorizationHeaderBuilder::new( 14 | //! "GET", &url, "consumer", "secret", SignatureMethod::HmacSha1) 15 | //! .token("token", "secret") 16 | //! .finish(); 17 | //! # } 18 | //! ``` 19 | //! 20 | //! If you use for Twitter, because of Twitter's bug, use `finish_for_twitter` method, 21 | //! and make sure to encode the request body with `OAUTH_ENCODE_SET`. 22 | //! For more detail, see [this article](http://azyobuzin.hatenablog.com/entry/2015/04/18/232516) (Japanese). 23 | 24 | extern crate base64; 25 | extern crate rand; 26 | extern crate ring; 27 | extern crate time; 28 | pub extern crate url; 29 | 30 | #[cfg(test)] mod tests; 31 | 32 | use std::ascii::AsciiExt; 33 | use std::borrow::{Borrow, Cow}; 34 | use std::error::Error; 35 | use std::fmt::{self, Write}; 36 | use std::iter; 37 | use url::Url; 38 | use url::percent_encoding::{EncodeSet, PercentEncode, utf8_percent_encode}; 39 | 40 | /// Available `oauth_signature_method` types. 41 | #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash)] 42 | pub enum SignatureMethod { 43 | /// HMAC-SHA1 44 | HmacSha1, 45 | /// PLAINTEXT 46 | Plaintext 47 | } 48 | 49 | impl SignatureMethod { 50 | fn to_str(&self) -> &'static str { 51 | match *self { 52 | SignatureMethod::HmacSha1 => "HMAC-SHA1", 53 | SignatureMethod::Plaintext => "PLAINTEXT" 54 | } 55 | } 56 | } 57 | 58 | impl fmt::Display for SignatureMethod { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | f.write_str(self.to_str()) 61 | } 62 | } 63 | 64 | /// [RFC 5849 section 3.6](http://tools.ietf.org/html/rfc5849#section-3.6). 65 | #[derive(Copy, Clone)] 66 | #[allow(non_camel_case_types)] 67 | pub struct OAUTH_ENCODE_SET; 68 | 69 | impl EncodeSet for OAUTH_ENCODE_SET { 70 | fn contains(&self, byte: u8) -> bool { 71 | !((byte >= 0x30 && byte <= 0x39) 72 | || (byte >= 0x41 && byte <= 0x5A) 73 | || (byte >= 0x61 && byte <= 0x7A) 74 | || byte == 0x2D || byte == 0x2E 75 | || byte == 0x5F || byte == 0x7E) 76 | } 77 | } 78 | 79 | fn percent_encode(input: &str) -> PercentEncode { 80 | utf8_percent_encode(input, OAUTH_ENCODE_SET) 81 | } 82 | 83 | #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash)] 84 | pub enum ParseOAuthAuthorizationHeaderError { 85 | /// The input is violating auth-param format. 86 | FormatError, 87 | /// The input is not completely escaped with `OAUTH_ENCODE_SET`. 88 | EscapeError 89 | } 90 | 91 | impl Error for ParseOAuthAuthorizationHeaderError { 92 | fn description(&self) -> &str { 93 | match *self { 94 | ParseOAuthAuthorizationHeaderError::FormatError => "The input is violating auth-param format", 95 | ParseOAuthAuthorizationHeaderError::EscapeError => "The input is not completely escaped with `OAUTH_ENCODE_SET`" 96 | } 97 | } 98 | } 99 | 100 | impl fmt::Display for ParseOAuthAuthorizationHeaderError { 101 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 102 | f.write_str(self.description()) 103 | } 104 | } 105 | 106 | /// `Authorization` header for OAuth. 107 | /// 108 | /// # Example 109 | /// ``` 110 | /// # use oauthcli::OAuthAuthorizationHeader; 111 | /// let header: OAuthAuthorizationHeader = "oauth_consumer_key=\"foo\"".parse().unwrap(); 112 | /// assert_eq!(header.to_string(), "OAuth oauth_consumer_key=\"foo\""); 113 | /// ``` 114 | #[derive(Debug, Clone)] 115 | pub struct OAuthAuthorizationHeader { 116 | s: String 117 | } 118 | 119 | impl OAuthAuthorizationHeader { 120 | /// `auth-param` in RFC 7235 121 | pub fn auth_param(&self) -> &str { 122 | &self.s 123 | } 124 | 125 | pub fn auth_param_owned(self) -> String { 126 | self.s 127 | } 128 | } 129 | 130 | impl fmt::Display for OAuthAuthorizationHeader { 131 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 132 | try!(f.write_str("OAuth ")); 133 | f.write_str(&self.s) 134 | } 135 | } 136 | 137 | impl std::str::FromStr for OAuthAuthorizationHeader { 138 | type Err = ParseOAuthAuthorizationHeaderError; 139 | 140 | fn from_str(s: &str) -> Result { 141 | fn is_hex(x: u8) -> bool { 142 | (x >= b'0' && x <= b'9') || 143 | (x >= b'A' && x <= b'F') || 144 | (x >= b'a' && x <= b'f') 145 | } 146 | 147 | fn check(s: &str) -> bool { 148 | let mut i = s.as_bytes().iter(); 149 | while let Some(&c) = i.next() { 150 | match c { 151 | 0x25 => { 152 | if let (Some(&a), Some(&b)) = (i.next(), i.next()) { 153 | if !(is_hex(a) && is_hex(b)) { return false; } 154 | } else { 155 | return false; 156 | } 157 | }, 158 | c => if OAUTH_ENCODE_SET.contains(c) { return false; } 159 | } 160 | } 161 | 162 | true 163 | } 164 | 165 | // Remove "OAuth" scheme 166 | let mut s = s.trim(); 167 | if let Some(scheme) = s.split_whitespace().next() { 168 | if scheme.eq_ignore_ascii_case("OAuth") { 169 | s = &scheme[5..].trim_left(); 170 | } 171 | } 172 | 173 | // Check each pair 174 | for pair in s.split(',').map(|x| x.trim()).filter(|x| x.len() > 0) { 175 | if let Some(equal_index) = pair.find('=') { 176 | if !check(pair[0..equal_index].trim_right()) { 177 | return Err(ParseOAuthAuthorizationHeaderError::EscapeError); 178 | } 179 | 180 | let val = pair[equal_index+1..].trim_left(); 181 | if !val.starts_with('"') || !val.ends_with('"') { 182 | return Err(ParseOAuthAuthorizationHeaderError::FormatError); 183 | } 184 | if !check(&val[1..val.len()-1]) { 185 | return Err(ParseOAuthAuthorizationHeaderError::EscapeError); 186 | } 187 | } else { 188 | return Err(ParseOAuthAuthorizationHeaderError::FormatError); 189 | } 190 | } 191 | 192 | Ok(OAuthAuthorizationHeader { s: s.to_owned() }) 193 | } 194 | } 195 | 196 | fn base_string_url(url: &Url) -> String { 197 | let scheme = url.scheme(); 198 | 199 | let mut result = String::with_capacity(url.as_str().len()); 200 | result.push_str(scheme); 201 | result.push_str("://"); 202 | result.push_str(url.host_str().expect("The host is None")); 203 | 204 | if let Some(p) = url.port() { 205 | match (scheme, p) { 206 | ("http", 80) | ("https", 443) => (), 207 | ("http", p) | ("https", p) => write!(&mut result, ":{}", p).unwrap(), 208 | _ => panic!("The scheme is not \"http\" or \"https\"") 209 | } 210 | } 211 | 212 | result.push_str(url.path()); 213 | result 214 | } 215 | 216 | struct PercentEncodedParameters<'a>(Vec<(Cow<'a, str>, Cow<'a, str>)>); 217 | 218 | fn percent_encode_parameters<'a, P>(params: P) -> PercentEncodedParameters<'a> 219 | where P: Iterator, Cow<'a, str>)> 220 | { 221 | PercentEncodedParameters( 222 | params 223 | .map(|(k, v)| (percent_encode(&k).to_string().into(), percent_encode(&v).to_string().into())) 224 | .collect() 225 | ) 226 | } 227 | 228 | fn normalize_parameters<'a>(params: PercentEncodedParameters<'a>) -> String { 229 | let mut mutparams = params.0; 230 | let mut result = String::new(); 231 | 232 | if mutparams.len() > 0 { 233 | mutparams.sort(); 234 | 235 | let mut first = true; 236 | for (key, val) in mutparams.into_iter() { 237 | if first { first = false; } 238 | else { result.push('&'); } 239 | 240 | result.push_str(&key); 241 | result.push('='); 242 | result.push_str(&val); 243 | } 244 | } 245 | 246 | result 247 | } 248 | 249 | fn gen_timestamp() -> u64 { 250 | let x = time::now_utc().to_timespec().sec; 251 | assert!(x > 0); 252 | return x as u64; 253 | } 254 | 255 | /// Generate a string for `oauth_nonce`. 256 | fn nonce() -> String { 257 | use rand::Rng; 258 | 259 | rand::thread_rng().gen_ascii_chars() 260 | .take(42).collect() 261 | } 262 | 263 | fn hmac_sha1_base64(key: &[u8], msg: &[u8]) -> String { 264 | use ring::{digest, hmac}; 265 | 266 | base64::encode( 267 | hmac::sign( 268 | &hmac::SigningKey::new(&digest::SHA1, key), 269 | msg 270 | ).as_ref() 271 | ) 272 | } 273 | 274 | pub struct OAuthAuthorizationHeaderBuilder<'a> { 275 | method: Cow<'a, str>, 276 | url: &'a Url, 277 | parameters: Vec<(Cow<'a, str>, Cow<'a, str>)>, 278 | consumer_key: Cow<'a, str>, 279 | consumer_secret: Cow<'a, str>, 280 | signature_method: SignatureMethod, 281 | realm: Option>, 282 | token: Option>, 283 | token_secret: Option>, 284 | timestamp: Option, 285 | nonce: Option>, 286 | callback: Option>, 287 | verifier: Option>, 288 | include_version: bool 289 | } 290 | 291 | impl<'a> OAuthAuthorizationHeaderBuilder<'a> { 292 | pub fn new(method: M, url: &'a Url, consumer_key: C, consumer_secret: S, signature_method: SignatureMethod) -> Self 293 | where M: Into>, C: Into>, S: Into> 294 | { 295 | OAuthAuthorizationHeaderBuilder { 296 | method: method.into(), 297 | url: url, 298 | parameters: Vec::new(), 299 | consumer_key: consumer_key.into(), 300 | consumer_secret: consumer_secret.into(), 301 | signature_method: signature_method, 302 | realm: None, 303 | token: None, 304 | token_secret: None, 305 | timestamp: None, 306 | nonce: None, 307 | callback: None, 308 | verifier: None, 309 | include_version: true 310 | } 311 | } 312 | 313 | pub fn request_parameters(&mut self, parameters: P) -> &mut Self 314 | where K: Into>, V: Into>, P: IntoIterator 315 | { 316 | self.parameters.extend(parameters.into_iter().map(|(k, v)| (k.into(), v.into()))); 317 | self 318 | } 319 | 320 | pub fn realm>>(&mut self, realm: T) -> &mut Self { 321 | self.realm = Some(realm.into()); 322 | self 323 | } 324 | 325 | pub fn token(&mut self, token: T, secret: S) -> &mut Self 326 | where T: Into>, S: Into> 327 | { 328 | self.token = Some(token.into()); 329 | self.token_secret = Some(secret.into()); 330 | self 331 | } 332 | 333 | /// Sets a custom timestamp. 334 | /// If you don't call `timestamp()`, the current time will be used. 335 | pub fn timestamp(&mut self, timestamp: u64) -> &mut Self { 336 | self.timestamp = Some(timestamp); 337 | self 338 | } 339 | 340 | /// Sets a custom nonce. 341 | /// If you don't call `nonce()`, a random string will be used. 342 | pub fn nonce>>(&mut self, nonce: T) -> &mut Self { 343 | self.nonce = Some(nonce.into()); 344 | self 345 | } 346 | 347 | pub fn callback>>(&mut self, callback: T) -> &mut Self { 348 | self.callback = Some(callback.into()); 349 | self 350 | } 351 | 352 | pub fn verifier>>(&mut self, verifier: T) -> &mut Self { 353 | self.verifier = Some(verifier.into()); 354 | self 355 | } 356 | 357 | /// Sets the value that indicates whether the builder includes `"oauth_version"` parameter. 358 | /// The default is `true`. 359 | pub fn include_version(&mut self, include_version: bool) -> &mut Self { 360 | self.include_version = include_version; 361 | self 362 | } 363 | 364 | fn signature(&self, oauth_params: &[(&'a str, &'a str)], for_twitter: bool) -> String { 365 | let mut key: String = percent_encode(&self.consumer_secret).collect(); 366 | key.push('&'); 367 | 368 | if let &Some(ref x) = &self.token_secret { 369 | key.extend(percent_encode(&x)); 370 | } 371 | 372 | match &self.signature_method { 373 | &SignatureMethod::HmacSha1 => { 374 | let params = oauth_params.iter() 375 | .map(|&(k, v)| (k.into(), v.into())) 376 | .chain(self.parameters.iter() 377 | .map(|&(ref k, ref v)| (Cow::Borrowed(k.borrow()), Cow::Borrowed(v.borrow())))); 378 | 379 | let params = 380 | if for_twitter { 381 | // Workaround for Twitter: don't re-encode the query 382 | let PercentEncodedParameters(mut x) = percent_encode_parameters(params); 383 | 384 | if let Some(query) = self.url.query() { 385 | for pair in query.split('&').filter(|x| x.len() > 0) { 386 | let mut pair_iter = pair.splitn(2, '='); 387 | let key = pair_iter.next().unwrap(); 388 | let val = pair_iter.next().unwrap_or(""); 389 | x.push((key.into(), val.into())); 390 | } 391 | } 392 | 393 | PercentEncodedParameters(x) 394 | } else { 395 | percent_encode_parameters(params.chain(self.url.query_pairs())) 396 | }; 397 | 398 | let mut base_string = self.method.to_ascii_uppercase(); 399 | base_string.push('&'); 400 | base_string.extend(percent_encode(&base_string_url(self.url))); 401 | base_string.push('&'); 402 | base_string.extend(percent_encode(&normalize_parameters(params))); 403 | 404 | hmac_sha1_base64(key.as_bytes(), base_string.as_bytes()) 405 | }, 406 | &SignatureMethod::Plaintext => key 407 | } 408 | } 409 | 410 | fn finish_impl(&self, for_twitter: bool) -> OAuthAuthorizationHeader { 411 | let tmp_timestamp = self.timestamp.unwrap_or_else(gen_timestamp).to_string(); 412 | let tmp_nonce; 413 | let oauth_params = { 414 | let mut p = Vec::with_capacity(8); 415 | 416 | p.push(("oauth_consumer_key", self.consumer_key.borrow())); 417 | if let Some(ref x) = self.token { p.push(("oauth_token", x.borrow())) } 418 | p.push(("oauth_signature_method", self.signature_method.to_str())); 419 | p.push(("oauth_timestamp", &tmp_timestamp)); 420 | p.push(("oauth_nonce", match &self.nonce { 421 | &Some(ref x) => x.borrow(), 422 | _ => { 423 | tmp_nonce = nonce(); 424 | &tmp_nonce 425 | } 426 | })); 427 | if let &Some(ref x) = &self.callback { p.push(("oauth_callback", x.borrow())) } 428 | if let &Some(ref x) = &self.verifier { p.push(("oauth_verifier", x.borrow())) } 429 | if self.include_version { p.push(("oauth_version", "1.0")) } 430 | 431 | p 432 | }; 433 | 434 | let signature = self.signature(&oauth_params, for_twitter); 435 | 436 | let mut oauth_params = self.realm.as_ref() 437 | .map(|x| ("realm", x.borrow())) 438 | .into_iter() 439 | .chain(oauth_params.into_iter()) 440 | .chain(iter::once(("oauth_signature", &signature[..]))); 441 | 442 | let mut result = String::new(); 443 | let mut first = true; 444 | 445 | while let Some((k, v)) = oauth_params.next() { 446 | if first { first = false; } 447 | else { result.push(','); } 448 | 449 | write!(&mut result, "{}=\"{}\"", 450 | percent_encode(k), percent_encode(v)).unwrap(); 451 | } 452 | 453 | OAuthAuthorizationHeader { s: result } 454 | } 455 | 456 | /// Generate `Authorization` header for OAuth. 457 | /// 458 | /// # Panics 459 | /// This function will panic if `url` is not valid for HTTP or HTTPS. 460 | pub fn finish(&self) -> OAuthAuthorizationHeader { 461 | self.finish_impl(false) 462 | } 463 | 464 | /// Generate `Authorization` header for Twitter. 465 | /// 466 | /// # Panics 467 | /// This function will panic if `url` is not valid for HTTP or HTTPS. 468 | pub fn finish_for_twitter(&self) -> OAuthAuthorizationHeader { 469 | self.finish_impl(true) 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | use super::*; 3 | 4 | #[test] 5 | fn signature_method_test() { 6 | assert_eq!( 7 | SignatureMethod::HmacSha1.to_string(), 8 | "HMAC-SHA1" 9 | ); 10 | assert_eq!( 11 | SignatureMethod::Plaintext.to_string(), 12 | "PLAINTEXT" 13 | ); 14 | } 15 | 16 | #[test] 17 | fn percent_encode_test() { 18 | use super::percent_encode; 19 | 20 | // https://dev.twitter.com/oauth/overview/percent-encoding-parameters 21 | 22 | assert_eq!( 23 | percent_encode("Ladies + Gentlemen").to_string(), 24 | "Ladies%20%2B%20Gentlemen" 25 | ); 26 | assert_eq!( 27 | percent_encode("An encoded string!").to_string(), 28 | "An%20encoded%20string%21" 29 | ); 30 | assert_eq!( 31 | percent_encode("Dogs, Cats & Mice").to_string(), 32 | "Dogs%2C%20Cats%20%26%20Mice" 33 | ); 34 | assert_eq!( 35 | percent_encode("☃").to_string(), 36 | "%E2%98%83" 37 | ); 38 | } 39 | 40 | #[test] 41 | fn from_str_test() { 42 | fn f(s: &str) -> Result { 43 | s.parse() 44 | } 45 | 46 | assert!(f(",a = \"a%2F\" , b = \"b\",,").is_ok()); 47 | assert!(f("a").is_err()); 48 | assert!(f("a=\"+a\"").is_err()); 49 | 50 | assert!(f(r#"oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", 51 | oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", 52 | oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", 53 | oauth_signature_method="HMAC-SHA1", 54 | oauth_timestamp="1318622958", 55 | oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", 56 | oauth_version="1.0""#).is_ok()); 57 | } 58 | 59 | #[test] 60 | fn base_string_url_test() { 61 | use super::base_string_url; 62 | 63 | assert_eq!( 64 | base_string_url( 65 | &Url::parse("HTTP://EXAMPLE.COM:80/r%20v/X?id=123").unwrap() 66 | ), 67 | "http://example.com/r%20v/X" 68 | ); 69 | assert_eq!( 70 | base_string_url( 71 | &Url::parse("https://www.example.net:8080/?q=1").unwrap() 72 | ), 73 | "https://www.example.net:8080/" 74 | ); 75 | } 76 | 77 | #[test] 78 | fn normalize_parameters_test() { 79 | use super::{normalize_parameters, percent_encode_parameters}; 80 | 81 | let params = [ 82 | ("b5", "=%3D"), 83 | ("a3", "a"), 84 | ("c@", ""), 85 | ("a2", "r b"), 86 | ("oauth_consumer_key", "9djdj82h48djs9d2"), 87 | ("oauth_token", "kkk9d7dh3k39sjv7"), 88 | ("oauth_signature_method", "HMAC-SHA1"), 89 | ("oauth_timestamp", "137131201"), 90 | ("oauth_nonce", "7d8f3e4a"), 91 | ("c2", ""), 92 | ("a3", "2 q") 93 | ]; 94 | 95 | assert_eq!( 96 | normalize_parameters( 97 | percent_encode_parameters( 98 | params.into_iter().map(|&(k, v)| (k.into(), v.into())) 99 | ) 100 | ), 101 | concat!( 102 | "a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj", 103 | "dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1", 104 | "&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7" 105 | ) 106 | ); 107 | } 108 | 109 | // https://tools.ietf.org/html/rfc5849#section-1.2 110 | 111 | #[test] 112 | fn example_initiate() { 113 | let url = Url::parse("https://photos.example.net/initiate").unwrap(); 114 | let result = OAuthAuthorizationHeaderBuilder::new("POST", &url, "dpf43f3p2l4k3l03", "kd94hf93k423kf44", SignatureMethod::HmacSha1) 115 | .realm("Photos") 116 | .timestamp(137131200) 117 | .nonce("wIjqoS") 118 | .callback("http://printer.example.com/ready") 119 | .include_version(false) 120 | .finish(); 121 | 122 | assert_eq!( 123 | result.to_string(), 124 | "OAuth realm=\"Photos\",\ 125 | oauth_consumer_key=\"dpf43f3p2l4k3l03\",\ 126 | oauth_signature_method=\"HMAC-SHA1\",\ 127 | oauth_timestamp=\"137131200\",\ 128 | oauth_nonce=\"wIjqoS\",\ 129 | oauth_callback=\"http%3A%2F%2Fprinter.example.com%2Fready\",\ 130 | oauth_signature=\"74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D\"" 131 | ); 132 | } 133 | 134 | #[test] 135 | fn example_token() { 136 | let url = Url::parse("https://photos.example.net/token").unwrap(); 137 | let result = OAuthAuthorizationHeaderBuilder::new("POST", &url, "dpf43f3p2l4k3l03", "kd94hf93k423kf44", SignatureMethod::HmacSha1) 138 | .realm("Photos") 139 | .token("hh5s93j4hdidpola", "hdhd0244k9j7ao03") 140 | .timestamp(137131201) 141 | .nonce("walatlh") 142 | .verifier("hfdp7dh39dks9884") 143 | .include_version(false) 144 | .finish(); 145 | 146 | assert_eq!( 147 | result.to_string(), 148 | "OAuth realm=\"Photos\",\ 149 | oauth_consumer_key=\"dpf43f3p2l4k3l03\",\ 150 | oauth_token=\"hh5s93j4hdidpola\",\ 151 | oauth_signature_method=\"HMAC-SHA1\",\ 152 | oauth_timestamp=\"137131201\",\ 153 | oauth_nonce=\"walatlh\",\ 154 | oauth_verifier=\"hfdp7dh39dks9884\",\ 155 | oauth_signature=\"gKgrFCywp7rO0OXSjdot%2FIHF7IU%3D\"" 156 | ); 157 | } 158 | 159 | #[test] 160 | fn example_photos() { 161 | let url = Url::parse("http://photos.example.net/photos?file=vacation.jpg&size=original").unwrap(); 162 | let result = OAuthAuthorizationHeaderBuilder::new("GET", &url, "dpf43f3p2l4k3l03", "kd94hf93k423kf44", SignatureMethod::HmacSha1) 163 | .realm("Photos") 164 | .token("nnch734d00sl2jdk", "pfkkdhi9sl3r4s00") 165 | .timestamp(137131202) 166 | .nonce("chapoH") 167 | .include_version(false) 168 | .finish(); 169 | 170 | assert_eq!( 171 | result.to_string(), 172 | "OAuth realm=\"Photos\",\ 173 | oauth_consumer_key=\"dpf43f3p2l4k3l03\",\ 174 | oauth_token=\"nnch734d00sl2jdk\",\ 175 | oauth_signature_method=\"HMAC-SHA1\",\ 176 | oauth_timestamp=\"137131202\",\ 177 | oauth_nonce=\"chapoH\",\ 178 | oauth_signature=\"MdpQcU8iPSUjWoN%2FUDMsK2sui9I%3D\"" 179 | ); 180 | } 181 | --------------------------------------------------------------------------------