send to cnut
This commit is contained in:
parent
6d4265478d
commit
d4a7107978
1161
Cargo.lock
generated
1161
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
194
src/main.rs
194
src/main.rs
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue