flickr_api/
oauth.rs

1use crate::Resultable;
2use data_encoding::BASE64;
3use hmac::{Hmac, Mac};
4use itertools::Itertools;
5use serde::{Deserialize, Serialize};
6use std::time::{SystemTime, UNIX_EPOCH};
7use url::form_urlencoded;
8
9type HmacSha1 = Hmac<sha1::Sha1>;
10
11/// OAuth token
12#[derive(Serialize, Deserialize, Default, Clone, Debug)]
13pub struct Token {
14    pub token: String,
15    pub secret: String,
16}
17
18/// flickr API key
19#[derive(Serialize, Deserialize, Default, Clone, Debug)]
20pub struct ApiKey {
21    pub key: String,
22    pub secret: String,
23}
24
25#[derive(Debug, Deserialize)]
26#[serde(untagged)]
27pub enum OauthAccessAnswer {
28    Ok(OauthAccessGranted),
29    Err(OauthErrorDescription),
30}
31
32#[derive(Debug, Deserialize)]
33#[serde(untagged)]
34pub enum OauthTokenAnswer {
35    Ok(OauthTokenGranted),
36    Err(OauthErrorDescription),
37}
38
39impl Resultable<Token, String> for OauthTokenAnswer {
40    fn to_result(self) -> Result<Token, String> {
41        match self {
42            OauthTokenAnswer::Ok(OauthTokenGranted {
43                oauth_callback_confirmed: _,
44                oauth_token: token,
45                oauth_token_secret: secret,
46            }) => Ok(Token { token, secret }),
47            OauthTokenAnswer::Err(e) => Err(e.oauth_problem),
48        }
49    }
50}
51
52#[derive(Debug, Default, Deserialize)]
53pub struct OauthAccessGranted {
54    pub fullname: String,
55    pub username: String,
56    pub user_nsid: String,
57    pub oauth_token: String,
58    pub oauth_token_secret: String,
59}
60
61#[derive(Debug, Default, Deserialize)]
62pub struct OauthTokenGranted {
63    pub oauth_callback_confirmed: String,
64    pub oauth_token: String,
65    pub oauth_token_secret: String,
66}
67
68#[derive(Debug, Default, Deserialize)]
69pub struct OauthErrorDescription {
70    pub oauth_problem: String,
71    #[serde(default)]
72    pub debug_sbs: String,
73}
74
75impl Resultable<OauthAccessGranted, String> for OauthAccessAnswer {
76    fn to_result(self) -> Result<OauthAccessGranted, String> {
77        match self {
78            OauthAccessAnswer::Ok(k) => Ok(k),
79            OauthAccessAnswer::Err(e) => Err(e.oauth_problem),
80        }
81    }
82}
83
84impl Resultable<Token, String> for OauthAccessAnswer {
85    fn to_result(self) -> Result<Token, String> {
86        match self {
87            OauthAccessAnswer::Ok(OauthAccessGranted {
88                fullname: _,
89                username: _,
90                user_nsid: _,
91                oauth_token: token,
92                oauth_token_secret: secret,
93            }) => Ok(Token { token, secret }),
94            OauthAccessAnswer::Err(e) => Err(e.oauth_problem),
95        }
96    }
97}
98
99/// The type of request that this URL expects
100///
101/// This is used by the signature algorithm and needs to match what is done later.
102pub enum RequestTarget<'a> {
103    Get(&'a str),
104    Post(&'a str),
105}
106
107impl<'a> RequestTarget<'a> {
108    fn uri(&'a self) -> &'a str {
109        match self {
110            RequestTarget::Get(val) => val,
111            RequestTarget::Post(val) => val,
112        }
113    }
114}
115
116/// Prepares a request to be sent with authentication
117///
118/// This supplements the given parameters with a signature and necessary oauth fields. This methods
119/// is dependent on the [RequestTarget] enum indicating which protocol to use as this influences
120/// the signature.
121pub fn build_request(
122    target: RequestTarget,
123    params: &mut Vec<(&'static str, String)>,
124    api: &ApiKey,
125    oauth: Option<&Token>,
126) {
127    let seconds = SystemTime::now()
128        .duration_since(UNIX_EPOCH)
129        .unwrap()
130        .as_secs()
131        .to_string();
132
133    let nonce = seconds[1..9].to_string();
134
135    params.extend(vec![
136        ("oauth_consumer_key", api.key.clone()),
137        ("oauth_nonce", nonce),
138        ("oauth_signature_method", "HMAC-SHA1".to_string()),
139        ("oauth_timestamp", seconds),
140        ("oauth_version", "1.0".to_string()),
141    ]);
142
143    let key: String;
144
145    match &oauth {
146        Some(value) => {
147            params.extend(vec![("oauth_token", value.token.clone())]);
148            key = format!("{}&{}", api.secret, value.secret)
149        }
150        None => key = format!("{}&", api.secret),
151    };
152
153    params.sort_by(|a, b| a.0.cmp(b.0));
154
155    let to_sign = params
156        .iter()
157        .filter(|(k, _)| !vec!["photo"].contains(k))
158        .map(|(a, b)| {
159            format!(
160                "{a}={}",
161                form_urlencoded::byte_serialize(&b.as_bytes()).collect::<String>(),
162            )
163        })
164        .join("&");
165
166    let uri = target.uri();
167    let method = match target {
168        RequestTarget::Get(_) => "GET",
169        RequestTarget::Post(_) => "POST",
170    };
171
172    let raw = format!(
173        "{method}&{}&{}",
174        form_urlencoded::byte_serialize(&uri.as_bytes()).collect::<String>(),
175        form_urlencoded::byte_serialize(&to_sign.as_bytes()).collect::<String>()
176    );
177
178    let mut mac = HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
179    mac.update(raw.as_bytes());
180    let signature: String = BASE64.encode(&mac.finalize().into_bytes());
181
182    params.push(("oauth_signature", signature));
183}