From 7b84985857fd9bd1756439383f1a1ae82f9bd57a Mon Sep 17 00:00:00 2001 From: iximeow Date: Mon, 15 Jan 2018 12:21:27 -0800 Subject: ensure all query string parameters are properly escaped also un-escape html-encoded characters in DMs also distinguish errors in auth commands --- src/commands/auth.rs | 28 +++++++++++--- src/commands/dm.rs | 7 +++- src/commands/fav.rs | 4 +- src/commands/follow.rs | 5 ++- src/commands/twete.rs | 33 +++++++++------- src/main.rs | 102 ++++++++++++++++++++++++++----------------------- src/tw/mod.rs | 15 ++++---- 7 files changed, 115 insertions(+), 79 deletions(-) diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 17503d5..08588dd 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -29,7 +29,14 @@ fn auth(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, dis // callback set to oob so the user will later get a PIN. // step 1: now present the correect oauth/authorize URL // this is as far as auth can get (rest depends on user PIN'ing with the right thing) - let res = queryer.raw_issue_request(::signed_api_req(&format!("{}?oauth_callback=oob", OAUTH_REQUEST_TOKEN_URL), hyper::Method::Post, &tweeter.app_key)); + let res = queryer.raw_issue_request( + ::signed_api_req( + OAUTH_REQUEST_TOKEN_URL, + &vec![("oauth_callback", "oob")], + hyper::Method::Post, + &tweeter.app_key + ) + ); match res { Ok(bytes) => match std::str::from_utf8(&bytes) { @@ -49,7 +56,7 @@ fn auth(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, dis display_info.status("couldn't rebuild url".to_owned()) }, Err(e) => - display_info.status(format!("request token url error: {}", e)) + display_info.status(format!("error starting auth: {}", e)) }; } @@ -67,7 +74,15 @@ fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp return; } - let res = queryer.raw_issue_request(::signed_api_req_with_token(&format!("{}?oauth_verifier={}", OAUTH_ACCESS_TOKEN_URL, line), hyper::Method::Post, &tweeter.app_key, &tweeter.WIP_auth.clone().unwrap())); + let res = queryer.raw_issue_request( + ::signed_api_req_with_token( + OAUTH_ACCESS_TOKEN_URL, + &vec![("oauth_verifier", &line)], + hyper::Method::Post, + &tweeter.app_key, + &tweeter.WIP_auth.clone().unwrap() + ) + ); match res { Ok(bytes) => match std::str::from_utf8(&bytes) { @@ -97,7 +112,7 @@ fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp secret: as_map["oauth_token_secret"].to_owned() }; - match queryer.do_api_get(::ACCOUNT_SETTINGS_URL, &tweeter.app_key, &user_credential) { + match queryer.do_api_get_noparam(::ACCOUNT_SETTINGS_URL, &tweeter.app_key, &user_credential) { Ok(settings) => { let user_handle = settings["screen_name"].as_str().unwrap().to_owned(); /* @@ -105,7 +120,8 @@ fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp * largely the same logic as `look_up_user`. */ let looked_up_user = queryer.do_api_get( - &format!("{}?screen_name={}", ::USER_LOOKUP_URL, user_handle), + ::USER_LOOKUP_URL, + &vec![("screen_name", &user_handle)], &tweeter.app_key, &user_credential ).and_then(|json| tw::user::User::from_json(json)); @@ -136,6 +152,6 @@ fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp display_info.status("couldn't rebuild url".to_owned()) }, Err(e) => - display_info.status(format!("request token url error: {}", e)) + display_info.status(format!("pin submission error: {}", e)) }; } diff --git a/src/commands/dm.rs b/src/commands/dm.rs index 95f65b7..f6fcbfd 100644 --- a/src/commands/dm.rs +++ b/src/commands/dm.rs @@ -42,7 +42,12 @@ fn dm(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, displ let encoded = ::url_encode(dm_text); let result = match tweeter.current_profile() { Some(user_profile) => { - queryer.do_api_post(&format!("{}?text={}&screen_name={}", DM_CREATE_URL, encoded, normalized_handle), &tweeter.app_key, &user_profile.creds) + queryer.do_api_post( + DM_CREATE_URL, + &vec![("text", &encoded), ("screen_name", &normalized_handle)], + &tweeter.app_key, + &user_profile.creds + ) }, None => Err("No logged in user to DM as".to_owned()) }; diff --git a/src/commands/fav.rs b/src/commands/fav.rs index 02ec7dd..d853a0d 100644 --- a/src/commands/fav.rs +++ b/src/commands/fav.rs @@ -23,7 +23,7 @@ fn unfav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, di Ok(twid) => { if let Some(twete) = tweeter.retrieve_tweet(&twid) { let result = match tweeter.current_profile() { - Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), + Some(user_profile) => queryer.do_api_post(UNFAV_TWEET_URL, &vec![("id", &twete.id)], &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to unfav from".to_owned()) }; match result { @@ -55,7 +55,7 @@ fn fav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp // tweeter.to_twitter_tweet_id(twid)... if let Some(twete) = tweeter.retrieve_tweet(&twid) { let result = match tweeter.current_profile() { - Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), + Some(user_profile) => queryer.do_api_post(FAV_TWEET_URL, &vec![("id", &twete.id)], &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to fav from".to_owned()) }; match result { diff --git a/src/commands/follow.rs b/src/commands/follow.rs index bc767d5..cd046c6 100644 --- a/src/commands/follow.rs +++ b/src/commands/follow.rs @@ -19,7 +19,7 @@ fn unfl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, dis let screen_name = line.trim(); let result = match tweeter.current_profile() { Some(user_profile) => { - queryer.do_api_post(&format!("{}?screen_name={}", FOLLOW_URL, screen_name), &tweeter.app_key, &user_profile.creds) + queryer.do_api_post(FOLLOW_URL, &vec![("screen_name", &screen_name)], &tweeter.app_key, &user_profile.creds) }, None => Err("No logged in user to unfollow from".to_owned()) }; @@ -45,7 +45,8 @@ fn fl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, displ format!( "fl resp: {:?}", queryer.do_api_post( - &format!("{}?screen_name={}", UNFOLLOW_URL, screen_name), + UNFOLLOW_URL, + &vec![("screen_name", &screen_name)], &tweeter.app_key, &user_profile.creds ) diff --git a/src/commands/twete.rs b/src/commands/twete.rs index 450c225..eded0db 100644 --- a/src/commands/twete.rs +++ b/src/commands/twete.rs @@ -24,7 +24,7 @@ fn del(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp // TODO this really converts twid to a TweetId::Twitter if let Some(twitter_id) = tweeter.retrieve_tweet(&twid).map(|x| x.id.to_owned()) { let result = match tweeter.current_profile() { - Some(user_profile) => queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds), + Some(user_profile) => queryer.do_api_post_noparam(&format!("{}/{}.json", DEL_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to delete as".to_owned()) }; match result { @@ -61,9 +61,13 @@ fn twete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, di } pub fn send_twete(text: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, display_info: &mut DisplayInfo) { - let substituted = ::url_encode(&text); let result = match tweeter.current_profile() { - Some(user_profile) => queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted), &tweeter.app_key, &user_profile.creds), + Some(user_profile) => queryer.do_api_post( + CREATE_TWEET_URL, + &vec![("status", &text)], + &tweeter.app_key, + &user_profile.creds + ), None => Err("No logged in user to tweet as".to_owned()) }; match result { @@ -202,7 +206,12 @@ pub fn send_reply(text: String, twid: TweetId, tweeter: &mut tw::TwitterCache, q let substituted = ::url_encode(&text); let result = match tweeter.current_profile() { Some(user_profile) => { - queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id), &tweeter.app_key, &user_creds) + queryer.do_api_post( + CREATE_TWEET_URL, + &vec![("status", &text), ("in_reply_to_status_id", &twete.id)], + &tweeter.app_key, + &user_creds + ) }, None => Err("No logged in user to tweet as".to_owned()) }; @@ -234,22 +243,18 @@ fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, di match maybe_id { Ok(twid) => { if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self - let substituted = ::url_encode(reply); - let attachment_url = ::url_encode( + let attachment_url = &format!( "https://www.twitter.com/{}/status/{}", tweeter.retrieve_user(&twete.author_id).unwrap().handle, // TODO: for now this is ok ish, if we got the tweet we have the author twete.id - ) - ); + ); let result = match tweeter.current_profile() { Some(user_profile) => { queryer.do_api_post( - &format!("{}?status={}&attachment_url={}", - CREATE_TWEET_URL, - substituted, - attachment_url - ), + CREATE_TWEET_URL, + &vec![("status", reply), ("attachment_url", attachment_url)], + &tweeter.app_key, &user_profile.creds ) @@ -291,7 +296,7 @@ fn retwete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, if let Some(twitter_id) = tweeter.retrieve_tweet(&twid).map(|x| x.id.to_owned()) { let result = match tweeter.current_profile() { Some(user_profile) => { - queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds) + queryer.do_api_post_noparam(&format!("{}/{}.json", RT_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds) }, None => Err("No logged in user to retweet as".to_owned()) }; diff --git a/src/main.rs b/src/main.rs index 603108c..ebeb155 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,11 +57,20 @@ pub struct Queryer { } impl Queryer { - fn do_api_get(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { - self.issue_request(signed_api_get(url, app_cred, user_cred)) + fn do_api_get_noparam(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.do_api_get(url, &vec![], app_cred, user_cred) } - fn do_api_post(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { - self.issue_request(signed_api_post(url, app_cred, user_cred)) + + fn do_api_get(&mut self, url: &str, params: &Vec<(&str, &str)>, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.issue_request(signed_api_get(url, params, app_cred, user_cred)) + } + + fn do_api_post_noparam(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.do_api_post(url, &vec![], app_cred, user_cred) + } + + fn do_api_post(&mut self, url: &str, params: &Vec<(&str, &str)>, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.issue_request(signed_api_post(url, params, app_cred, user_cred)) } /* fn do_web_req(&mut self, url: &str) -> Option { @@ -126,23 +135,27 @@ fn signed_web_get(url: &str) -> hyper::client::Request { } */ -fn signed_api_post(url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { - signed_api_req_with_token(url, Method::Post, app_cred, user_cred) +fn signed_api_post(url: &str, params: &Vec<(&str, &str)>, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + signed_api_req_with_token(url, params, Method::Post, app_cred, user_cred) +} + +fn signed_api_get(url: &str, params: &Vec<(&str, &str)>, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + signed_api_req_with_token(url, params, Method::Get, app_cred, user_cred) } -fn signed_api_get(url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { - signed_api_req_with_token(url, Method::Get, app_cred, user_cred) +fn signed_api_req_with_token(url: &str, params: &Vec<(&str, &str)>, method: Method, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + inner_signed_api_req(url, params, method, app_cred, Some(user_cred)) } -fn signed_api_req_with_token(url: &str, method: Method, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { - inner_signed_api_req(url, method, app_cred, Some(user_cred)) +fn signed_api_req_no_params(url: &str, method: Method, app_cred: &tw::Credential) -> hyper::client::Request { + inner_signed_api_req(url, &vec![], method, app_cred, None) } -fn signed_api_req(url: &str, method: Method, app_cred: &tw::Credential) -> hyper::client::Request { - inner_signed_api_req(url, method, app_cred, None) +fn signed_api_req(url: &str, params: &Vec<(&str, &str)>, method: Method, app_cred: &tw::Credential) -> hyper::client::Request { + inner_signed_api_req(url, params, method, app_cred, None) } -fn inner_signed_api_req(url: &str, method: Method, app_cred: &tw::Credential, maybe_user_cred: Option<&tw::Credential>) -> hyper::client::Request { +fn inner_signed_api_req(url: &str, params: &Vec<(&str, &str)>, method: Method, app_cred: &tw::Credential, maybe_user_cred: Option<&tw::Credential>) -> hyper::client::Request { // let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; let method_string = match method { Method::Get => "GET", @@ -150,10 +163,19 @@ fn inner_signed_api_req(url: &str, method: Method, app_cred: &tw::Credential, ma _ => panic!(format!("unsupported method {}", method)) }; - let params: Vec<(String, String)> = vec![]; - let _param_string: String = params.iter().map(|p| p.0.clone() + &"=".to_string() + &p.1).collect::>().join("&"); + let escaped = params.iter().map(|&(ref k, ref v)| format!("{}={}", + url_encode(k), + url_encode(v) + )); + let params_str = escaped.collect::>().join("&"); + + let constructed_url = if params_str.len() > 0 { + format!("{}?{}", url, params_str) + } else { + url.to_owned() + }; - let parsed_url = url::Url::parse(url).unwrap(); + let parsed_url = url::Url::parse(&constructed_url).unwrap(); let mut builder = oauthcli::OAuthAuthorizationHeaderBuilder::new( method_string, @@ -169,7 +191,7 @@ fn inner_signed_api_req(url: &str, method: Method, app_cred: &tw::Credential, ma let header = builder.finish(); - let mut req = Request::new(method, url.parse().unwrap()); + let mut req = Request::new(method, constructed_url.parse().unwrap()); { let headers = req.headers_mut(); @@ -479,35 +501,21 @@ fn do_ui( } fn url_encode(s: &str) -> String { - s - .replace("%", "%25") - .replace("+", "%2b") - .replace(" ", "+") - .replace("\\n", "%0a") - .replace("\\r", "%0d") - .replace("\\esc", "%1b") - .replace("!", "%21") - .replace("#", "%23") - .replace("$", "%24") - .replace("&", "%26") - .replace("'", "%27") - .replace("(", "%28") - .replace(")", "%29") - .replace("*", "%2a") - .replace(",", "%2c") - .replace("-", "%2d") - .replace(".", "%2e") - .replace("/", "%2f") - .replace(":", "%3a") - .replace(";", "%3b") - .replace("<", "%3c") - .replace("=", "%3d") - .replace(">", "%3e") - .replace("?", "%3f") - .replace("@", "%40") - .replace("[", "%5b") - .replace("\\", "%5c") - .replace("]", "%5d") + fn encode_byte(c: u8) -> String { + if c == 0x20 { + "+".to_string() + } else if (c > 0x40 && c <= 0x40 + 26) || + (c > 0x60 && c <= 0x60 + 26) || + (c >= 0x30 && c < 0x3a) || + c == 0x2d || c == 0x2e || c == 0x5f || c == 0x7e { + String::from_utf8(vec![c]).unwrap() + } else { + String::from(format!("%{:2x}", c)) + } + } + s.as_bytes().iter().map(|c| { + encode_byte(*c) + }).collect::>().join("") } // let (twete_tx, twete_rx) = chan::sync::>(0); @@ -536,7 +544,7 @@ fn connect_twitter_stream( .connector(connector) .build(&core.handle()); - let req = signed_api_get(STREAMURL, &app_cred, &user_cred); + let req = signed_api_get(STREAMURL, &vec![], &app_cred, &user_cred); let work = client.request(req).and_then(|res| { let status = res.status(); if status != hyper::StatusCode::Ok { diff --git a/src/tw/mod.rs b/src/tw/mod.rs index 9a14b11..63b8f07 100644 --- a/src/tw/mod.rs +++ b/src/tw/mod.rs @@ -571,10 +571,10 @@ impl TwitterProfile { } } pub fn get_settings(&self, queryer: &mut ::Queryer, app_key: &Credential) -> Result { - queryer.do_api_get(::ACCOUNT_SETTINGS_URL, app_key, &self.creds) + queryer.do_api_get_noparam(::ACCOUNT_SETTINGS_URL, app_key, &self.creds) } pub fn get_followers(&self, queryer: &mut ::Queryer, app_key: &Credential) -> Result { - queryer.do_api_get(::GET_FOLLOWER_IDS_URL, app_key, &self.creds) + queryer.do_api_get_noparam(::GET_FOLLOWER_IDS_URL, app_key, &self.creds) } pub fn set_following(&mut self, user_ids: Vec) -> (Vec, Vec) { let uid_set = user_ids.into_iter().collect::>(); @@ -1096,17 +1096,15 @@ impl TwitterCache { } fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Result { - let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); match self.current_profile() { - Some(ref user_profile) => queryer.do_api_get(url, &self.app_key, &user_profile.creds), + Some(ref user_profile) => queryer.do_api_get(::USER_LOOKUP_URL, &vec![("user_id", id)], &self.app_key, &user_profile.creds), None => Err("No authorized user to conduct lookup".to_owned()) } } fn look_up_tweet(&mut self, id: &str, queryer: &mut ::Queryer) -> Result { - let url = &format!("{}&id={}", ::TWEET_LOOKUP_URL, id); match self.current_profile() { - Some(ref user_profile) => queryer.do_api_get(url, &self.app_key, &user_profile.creds), + Some(ref user_profile) => queryer.do_api_get(::TWEET_LOOKUP_URL, &vec![("id", id)], &self.app_key, &user_profile.creds), None => Err("No authorized user to conduct lookup".to_owned()) } } @@ -1202,7 +1200,10 @@ fn handle_twitter_dm( // show DM tweeter.cache_api_user(structure["direct_message"]["recipient"].clone()); tweeter.cache_api_user(structure["direct_message"]["sender"].clone()); - let dm_text = structure["direct_message"]["text"].as_str().unwrap().to_string(); + let dm_text = structure["direct_message"]["text"].as_str().unwrap().to_string() + .replace("&", "&") + .replace(">", ">") + .replace("<", "<"); let to = structure["direct_message"]["recipient_id_str"].as_str().unwrap().to_string(); let from = structure["direct_message"]["sender_id_str"].as_str().unwrap().to_string(); display_info.recv(display::Infos::DM(dm_text, from, to)); -- cgit v1.1