use hex::ToHex;
use hmac::{Hmac, Mac};
use oauth2::{
basic::BasicClient, AuthType, AuthUrl, ClientId, ClientSecret, HttpRequest, HttpResponse,
ResourceOwnerPassword, ResourceOwnerUsername, SyncHttpClient, TokenUrl,
};
use sha2::Sha256;
use url::Url;
use std::env;
use std::time;
fn main() -> Result<(), anyhow::Error> {
let letterboxd_client_id = ClientId::new(
env::var("LETTERBOXD_CLIENT_ID")
.expect("Missing the LETTERBOXD_CLIENT_ID environment variable."),
);
let letterboxd_client_secret = ClientSecret::new(
env::var("LETTERBOXD_CLIENT_SECRET")
.expect("Missing the LETTERBOXD_CLIENT_SECRET environment variable."),
);
let auth_url = AuthUrl::new("https://api.letterboxd.com/api/v0/auth/404".to_string())?;
let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?;
let client = BasicClient::new(letterboxd_client_id.clone())
.set_client_secret(letterboxd_client_secret.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url);
let letterboxd_username = ResourceOwnerUsername::new(
env::var("LETTERBOXD_USERNAME")
.expect("Missing the LETTERBOXD_USERNAME environment variable."),
);
let letterboxd_password = ResourceOwnerPassword::new(
env::var("LETTERBOXD_PASSWORD")
.expect("Missing the LETTERBOXD_PASSWORD environment variable."),
);
let http_client = SigningHttpClient::new(letterboxd_client_id, letterboxd_client_secret);
let token_result = client
.set_auth_type(AuthType::RequestBody)
.exchange_password(&letterboxd_username, &letterboxd_password)
.request(&|request| http_client.execute(request))?;
println!("{token_result:?}");
Ok(())
}
#[derive(Debug, Clone)]
struct SigningHttpClient {
client_id: ClientId,
client_secret: ClientSecret,
inner: reqwest::blocking::Client,
}
impl SigningHttpClient {
fn new(client_id: ClientId, client_secret: ClientSecret) -> Self {
Self {
client_id,
client_secret,
inner: reqwest::blocking::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Client should build"),
}
}
fn execute(&self, mut request: HttpRequest) -> Result<HttpResponse, impl std::error::Error> {
let signed_url = self.sign_url(
Url::parse(&request.uri().to_string()).expect("http::Uri should be a valid url::Url"),
request.method(),
request.body(),
);
*request.uri_mut() = signed_url
.as_str()
.try_into()
.expect("url::Url should be a valid http::Uri");
self.inner.call(request)
}
fn sign_url(&self, mut url: Url, method: &http::method::Method, body: &[u8]) -> Url {
let nonce = uuid::Uuid::new_v4();
let timestamp = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.expect("SystemTime::duration_since failed")
.as_secs();
url.query_pairs_mut()
.append_pair("apikey", &self.client_id)
.append_pair("nonce", &format!("{}", nonce))
.append_pair("timestamp", &format!("{}", timestamp));
let mut hmac = Hmac::<Sha256>::new_from_slice(self.client_secret.secret().as_bytes())
.expect("HMAC can take key of any size");
hmac.update(method.as_str().as_bytes());
hmac.update(&[b'\0']);
hmac.update(url.as_str().as_bytes());
hmac.update(&[b'\0']);
hmac.update(body);
let signature: String = hmac.finalize().into_bytes().encode_hex();
url.query_pairs_mut().append_pair("signature", &signature);
url
}
}