send to cnut

This commit is contained in:
Alex Janka 2024-05-25 16:15:30 +10:00
parent 6d4265478d
commit d4a7107978
3 changed files with 790 additions and 569 deletions

1161
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tesla-auth-callback-watcher" name = "tesla-auth-callback-watcher"
version = "0.3.0" version = "1.0.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -11,7 +11,7 @@ log = "0.4.20"
env_logger = "0.11.1" env_logger = "0.11.1"
rand = "0.8" rand = "0.8"
lazy_static = "1.4" lazy_static = "1.4"
reqwest = "0.11" reqwest = { version = "0.12.4", features = ["json"] }
alex-utils = { git = "https://git.alexjanka.com/alex/alex-utils" } alex-utils = { git = "https://git.alexjanka.com/alex/alex-utils" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View file

@ -1,15 +1,7 @@
use std::{path::PathBuf, str::FromStr};
use alex_utils::PrintErrors;
use lazy_static::lazy_static;
use rand::Rng;
use reqwest::header;
use rocket::{ use rocket::{
request::Request, request::Request,
response::{Redirect, Responder}, response::{Redirect, Responder},
}; };
use serde::Deserialize;
use tesla_common::AuthInfo;
use thiserror::Error; use thiserror::Error;
#[macro_use] #[macro_use]
@ -28,33 +20,30 @@ fn rocket() -> _ {
) )
.init(); .init();
rocket::build().mount("/", routes![authenticated, deauthenticated, login]) rocket::build().mount("/", routes![authenticated])
} }
#[derive(Debug)] #[derive(Debug, Error)]
struct Secrets { enum Error {
client_id: String, #[error("missing field")]
client_secret: String, MissingField,
audience: String, #[error(transparent)]
redirect_uri: String, Reqwest(#[from] reqwest::Error),
} }
impl Secrets { impl<'a> Responder<'a, 'a> for Error {
fn load() -> Option<Self> { fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Some(Self { Err(match self {
client_id: std::env::var("CLIENT_ID").ok()?, Error::MissingField => rocket::http::Status::BadRequest,
client_secret: std::env::var("CLIENT_SECRET").ok()?, Error::Reqwest(_) => rocket::http::Status::ServiceUnavailable,
audience: std::env::var("AUDIENCE").ok()?,
redirect_uri: std::env::var("REDIRECT_URI").ok()?,
}) })
} }
} }
lazy_static! { lazy_static::lazy_static! {
static ref SECRETS: Option<Secrets> = Secrets::load(); static ref REDIR_URI: String = std::env::var("FORWARD_URI").ok().unwrap();
static ref KEY_DIR: Option<PathBuf> = std::env::var("TESLA_KEY_DIR") static ref FORWARD_URI:String=format!("{}/auth",REDIR_URI.as_str());
.ok()
.and_then(|v| PathBuf::from_str(v.as_str()).ok());
} }
#[get("/authenticated?<locale>&<code>&<state>&<issuer>")] #[get("/authenticated?<locale>&<code>&<state>&<issuer>")]
@ -63,155 +52,26 @@ async fn authenticated(
code: Option<String>, code: Option<String>,
state: Option<String>, state: Option<String>,
issuer: Option<String>, issuer: Option<String>,
) -> &'static str { ) -> Result<Redirect, Error> {
let (_, _, _) = (locale, state, issuer); let code = code.ok_or(Error::MissingField)?;
if let Some(code) = code { let state = state.ok_or(Error::MissingField)?;
match register_auth_key(code).await { let (_, _) = (locale, issuer);
Ok(_) => {}
Err(e) => log::error!("error registering: {e:?}"),
}
"authentication successful 🙂"
} else {
"authentication unsuccessful 😓"
}
}
#[derive(Deserialize, Debug)] reqwest::Client::builder()
#[allow(unused)] .build()?
struct TeslaError { .post(FORWARD_URI.as_str())
error: String, .json(&tesla_common::SuccessfulAuth { code, state })
error_description: String, .send()
#[serde(rename = "referenceID")] .await?;
reference_id: String, Ok(Redirect::to(REDIR_URI.as_str()))
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
enum AuthKeyError { enum AuthKeyError {
#[error("Error returned from Tesla")]
TeslaError(TeslaError),
#[error("Error deserialising json")] #[error("Error deserialising json")]
SerdeError(#[from] serde_json::Error), SerdeError(#[from] serde_json::Error),
} }
#[derive(Deserialize, Debug)]
#[allow(unused)]
struct AuthKeySuccess {
access_token: String,
refresh_token: String,
id_token: String,
expires_in: usize,
state: String,
token_type: String,
}
impl AuthKeySuccess {
fn get_keys(&self) -> AuthInfo {
AuthInfo {
access_token: tesla_common::AccessToken(self.access_token.clone()),
refresh_token: Some(tesla_common::RefreshToken(self.refresh_token.clone())),
}
}
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum AuthKeyResponse {
Ok(AuthKeySuccess),
Error(TeslaError),
}
async fn register_auth_key(code: String) -> Result<(), reqwest::Error> {
if let Some(secrets) = SECRETS.as_ref() {
let mut headers = header::HeaderMap::new();
headers.insert(
"Content-Type",
"application/x-www-form-urlencoded".parse().unwrap(),
);
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let res = client
.post("https://auth.tesla.com/oauth2/v3/token")
.headers(headers)
.body(
[
"grant_type=authorization_code&client_id=",
&secrets.client_id,
"&client_secret=",
&secrets.client_secret,
"&code=",
&code,
"&audience=",
&secrets.audience,
"&redirect_uri=",
&secrets.redirect_uri,
]
.concat(),
)
.send()
.await?
.text()
.await?;
let d = match serde_json::from_str(&res) {
Ok(AuthKeyResponse::Ok(v)) => Ok(v),
Ok(AuthKeyResponse::Error(e)) => Err(AuthKeyError::TeslaError(e)),
Err(e) => Err(AuthKeyError::SerdeError(e)),
};
match d {
Ok(keys) => save_auth(keys),
Err(e) => log::error!("error registering keys: {e:?}"),
}
} else {
log::error!("secrets not found - not registering code!!! code is {code}");
}
Ok(())
}
fn save_auth(keys: AuthKeySuccess) {
let auth_info = keys.get_keys();
let filename = KEY_DIR
.as_ref()
.map(|v| v.join(chrono::Local::now().to_rfc3339()));
match auth_info.save(filename.as_deref()) {
Ok(Some(v)) => log::warn!("could not save keys! keys: {v:?}"),
Ok(None) => log::warn!("saved keys to {filename:?}"),
Err(e) => log::error!("error saving keys: {e:?}"),
}
}
#[get("/deauthenticated/<_..>")]
fn deauthenticated(uri: &rocket::http::uri::Origin) -> &'static str {
log::error!("DEAUTHENTICATED: {uri:?}");
"deauthentication successful 🙂"
}
#[get("/")]
fn login() -> RedirOrStatic {
let state_random: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(16)
.map(char::from)
.collect();
SECRETS.as_ref().and_then(|secrets| {
rocket::http::uri::Absolute::parse_owned(format!(
"https://auth.tesla.com/oauth2/v3/authorize?&client_id={}&locale=en-US&prompt=login&redirect_uri={}&response_type=code&scope=openid%20vehicle_device_data%20offline_access%20user_data%20vehicle_cmds%20vehicle_charging_cmds&state={state_random}",
secrets.client_id,
secrets.redirect_uri
))
.some_or_print_with_context("failed to parse url")
})
.map(Redirect::to)
.map(Into::into)
.unwrap_or(format!(
"could not create redirect: secrets {} exist",
if SECRETS.is_some() { "does" } else { "does not" }
).into())
}
enum RedirOrStatic { enum RedirOrStatic {
Redirect(Box<Redirect>), Redirect(Box<Redirect>),
Static(String), Static(String),