From 9b2c0edcc5bfed80dc6ba7a67962cca1bf885cfd Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 15 Nov 2021 02:28:25 -0500 Subject: [PATCH] Twitch oauth enroll endpoint --- Cargo.lock | 1 + .../obs_gateway/src/endpoints/oauth_begin.rs | 55 +++- crates/app/obs_gateway/src/main.rs | 301 +++++++++--------- crates/app/obs_gateway/src/server_state.rs | 31 ++ crates/lib/http_server_common/Cargo.toml | 3 + .../http_server_common/src/response/mod.rs | 1 + .../src/response/to_json_success_response.rs | 11 + 7 files changed, 232 insertions(+), 171 deletions(-) create mode 100644 crates/app/obs_gateway/src/server_state.rs create mode 100644 crates/lib/http_server_common/src/response/to_json_success_response.rs diff --git a/Cargo.lock b/Cargo.lock index 7f66a5dba5..a2699de538 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1865,6 +1865,7 @@ dependencies = [ "anyhow", "base64 0.13.0", "chrono", + "container_common", "dotenv", "log", "serde", diff --git a/crates/app/obs_gateway/src/endpoints/oauth_begin.rs b/crates/app/obs_gateway/src/endpoints/oauth_begin.rs index 236a96debc..4f4cd5828c 100644 --- a/crates/app/obs_gateway/src/endpoints/oauth_begin.rs +++ b/crates/app/obs_gateway/src/endpoints/oauth_begin.rs @@ -2,16 +2,32 @@ use actix_http::{StatusCode, header}; use actix_web::{HttpRequest, web, HttpResponse, ResponseError, HttpResponseBuilder}; use crate::ObsGatewayServerState; use http_server_common::response::response_error_helpers::to_simple_json_error; +use http_server_common::response::response_success_helpers::simple_json_success; use log::info; use std::fmt; use std::sync::Arc; -use http_server_common::response::response_success_helpers::simple_json_success; +use twitch_oauth2::tokens::UserTokenBuilder; +use twitch_oauth2::{Scope, ClientSecret}; +use twitch_api2::twitch_oauth2::ClientId; +use http_server_common::response::to_json_success_response::to_json_success_response; + +#[derive(Serialize)] +pub struct OauthBeginEnrollResult { + redirect_url: String, +} #[derive(Debug)] pub enum OauthBeginEnrollError { ServerError, } +// NB: Not using DeriveMore since Clion doesn't understand it. +impl fmt::Display for OauthBeginEnrollError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "OauthBeginEnrollError") + } +} + impl ResponseError for OauthBeginEnrollError { fn status_code(&self) -> StatusCode { match *self { @@ -24,18 +40,11 @@ impl ResponseError for OauthBeginEnrollError { Self::ServerError => "server error".to_string(), }; - //to_simple_json_error(&error_reason, self.status_code()) + to_simple_json_error(&error_reason, self.status_code()) - HttpResponseBuilder::new(self.status_code()) - .set_header(header::CONTENT_TYPE, "application/json") - .body("TODO") - } -} - -// NB: Not using DeriveMore since Clion doesn't understand it. -impl fmt::Display for OauthBeginEnrollError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) + //HttpResponseBuilder::new(self.status_code()) + // .set_header(header::CONTENT_TYPE, "application/json") + // .body("TODO") } } @@ -44,7 +53,25 @@ pub async fn oauth_begin_enroll( server_state: web::Data> ) -> Result { + let redirect_url = + twitch_oauth2::url::Url::parse(&server_state.twitch_oauth_secrets.redirect_url).unwrap(); - info!("oauth enrollment begin"); - Ok(simple_json_success()) + let client_id = ClientId::new(&server_state.twitch_oauth_secrets.client_id); + let client_secret = ClientSecret::new(&server_state.twitch_oauth_secrets.client_secret); + + // TODO: Not all scopes are needed! + let mut builder = UserTokenBuilder::new(client_id, client_secret, redirect_url) + .set_scopes(Scope::all()) + .force_verify(true); + + let (url, _csrf_token) = builder.generate_url(); + + let response = OauthBeginEnrollResult { + redirect_url: url.to_string(), + }; + + let response = to_json_success_response(&response) + .map_err(|_| OauthBeginEnrollError::ServerError)?; + + Ok(response) } diff --git a/crates/app/obs_gateway/src/main.rs b/crates/app/obs_gateway/src/main.rs index e69ece9f53..35c5cd7562 100644 --- a/crates/app/obs_gateway/src/main.rs +++ b/crates/app/obs_gateway/src/main.rs @@ -14,6 +14,7 @@ pub mod endpoints; pub mod pubsub_gateway; +pub mod server_state; pub mod twitch; pub mod util; @@ -47,31 +48,12 @@ use twitch_api2::pubsub::Topic; use twitch_api2::pubsub; use twitch_oauth2::tokens::UserTokenBuilder; use twitch_oauth2::{AppAccessToken, Scope, TwitchToken, tokens::errors::AppAccessTokenError, ClientId, ClientSecret}; +use crate::server_state::{ObsGatewayServerState, EnvConfig, TwitchOauthSecrets}; -const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345"; +const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:54321"; pub type AnyhowResult = anyhow::Result; -/// State that is injected into every endpoint. -#[derive(Clone)] -pub struct ObsGatewayServerState { - /// Configuration from ENV vars. - /// Some of this might not be used. - pub env_config: EnvConfig, - pub hostname: String, -} - -#[derive(Clone)] -pub struct EnvConfig { - // Number of thread workers. - pub num_workers: usize, - pub bind_address: String, - pub cookie_domain: String, - pub cookie_secure: bool, - pub cookie_http_only: bool, - pub website_homepage_redirect: String, -} - #[actix_web::main] //#[tokio::main] async fn main() -> AnyhowResult<()> { @@ -122,126 +104,129 @@ async fn main() -> AnyhowResult<()> { //std::thread::sleep(Duration::from_secs(5000)); - // ==================== OAUTH FLOW ==================== - - println!("Oauth flow..."); - - let redirect_url = twitch_oauth2::url::Url::parse("http://localhost/test")?; - let mut builder = UserTokenBuilder::new(client_id, client_secret, redirect_url) - .set_scopes(Scope::all()) - .force_verify(true); - - //builder.add_scope(Scope::BitsRead); - - let (url, _csrf_token) = builder.generate_url(); - - println!("Go to this page: {}", url); - - - - let input = rpassword::prompt_password_stdout( - "Paste in the resulting adress after authenticating (input hidden): ", - )?; - - let u = twitch_oauth2::url::Url::parse(&input)?; - - let map: std::collections::HashMap<_, _> = u.query_pairs().collect(); - - let user_token = match (map.get("state"), map.get("code")) { - (Some(state), Some(code)) => { - let token = builder - .get_user_token( - &reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build()?, - state, - code, - ) - .await?; - println!("Got token: {:?}", token); - - token - } - _ => match (map.get("error"), map.get("error_description")) { - (std::option::Option::Some(error), std::option::Option::Some(error_description)) => { - anyhow::bail!( - "twitch errored with error: {} - {}", - error, - error_description - ); - } - _ => anyhow::bail!("invalid url passed"), - }, - }; - - let auth_token = user_token.access_token.as_str(); - - // ==================== PUBSUB SUBSCRIPTION AND MAIN LOOP ==================== - - let mut client = PollingTwitchWebsocketClient::new()?; - - println!("Connecting PubSub..."); - client.connect().await?; - - println!("Connected"); - - //println!("Starting polling thread..."); - //client.start_ping_thread().await; - - println!("Sending PING..."); - - client.send_ping().await?; - - println!("Try read next..."); - let r = client.try_next().await?; - println!("Result: {:?}", r); - - - println!("Begin LISTEN..."); - - let bit_topic = pubsub::channel_bits::ChannelBitsEventsV2 { - channel_id: user_id, - }.into_topic(); - - let topics = [bit_topic]; - - client.listen(&auth_token, &topics).await?; - - println!("Try read next..."); - let r = client.try_next().await?; - println!("Result: {:?}", r); - - /* - Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 { - channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: - Some(BadgeEntitlement { new_version: 100, previous_version: 1 }), bits_used: 100, - channel_id: "652567283", channel_name: "vocodes", chat_message: "Cheer100 testing the cheer", - context: Cheer, is_anonymous: false, time: "2021-09-27T04:30:53.627717085Z", - total_bits_used: 101, user_id: "650154491", user_name: "testytest512" }, - message_id: "793f745e-8f3e-5f71-bc41-4808c4b49a53", version: "1.0", is_anonymous: false } } }) - - Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 { - channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: None, - bits_used: 1, channel_id: "652567283", channel_name: "vocodes", - chat_message: "mario: hello everybody Cheer1", context: Cheer, is_anonymous: false, - time: "2021-09-27T04:47:32.943872952Z", total_bits_used: 102, user_id: "650154491", - user_name: "testytest512" }, message_id: "5844328d-495b-5585-9d6c-3c99949f9a0a", version: "1.0", - is_anonymous: false } } }) - */ - - - - println!("Try read next..."); - let r = client.try_next().await?; - println!("Result: {:?}", r); - - println!("Sleep..."); - std::thread::sleep(Duration::from_millis(30000)); - - //let text = msg.into_text()?; - //println!("Text: {}", text); - - // ======================================================== +// // ==================== OAUTH FLOW ==================== +// +// println!("Oauth flow..."); +// +// let redirect_url = twitch_oauth2::url::Url::parse("http://localhost/test")?; +// let mut builder = UserTokenBuilder::new( +// client_id.clone(), +// client_secret.clone(), +// redirect_url) +// .set_scopes(Scope::all()) +// .force_verify(true); +// +// //builder.add_scope(Scope::BitsRead); +// +// let (url, _csrf_token) = builder.generate_url(); +// +// println!("Go to this page: {}", url); +// +// +// +// let input = rpassword::prompt_password_stdout( +// "Paste in the resulting adress after authenticating (input hidden): ", +// )?; +// +// let u = twitch_oauth2::url::Url::parse(&input)?; +// +// let map: std::collections::HashMap<_, _> = u.query_pairs().collect(); +// +// let user_token = match (map.get("state"), map.get("code")) { +// (Some(state), Some(code)) => { +// let token = builder +// .get_user_token( +// &reqwest::Client::builder() +// .redirect(reqwest::redirect::Policy::none()) +// .build()?, +// state, +// code, +// ) +// .await?; +// println!("Got token: {:?}", token); +// +// token +// } +// _ => match (map.get("error"), map.get("error_description")) { +// (std::option::Option::Some(error), std::option::Option::Some(error_description)) => { +// anyhow::bail!( +// "twitch errored with error: {} - {}", +// error, +// error_description +// ); +// } +// _ => anyhow::bail!("invalid url passed"), +// }, +// }; +// +// let auth_token = user_token.access_token.as_str(); +// +// // ==================== PUBSUB SUBSCRIPTION AND MAIN LOOP ==================== +// +// let mut client = PollingTwitchWebsocketClient::new()?; +// +// println!("Connecting PubSub..."); +// client.connect().await?; +// +// println!("Connected"); +// +// //println!("Starting polling thread..."); +// //client.start_ping_thread().await; +// +// println!("Sending PING..."); +// +// client.send_ping().await?; +// +// println!("Try read next..."); +// let r = client.try_next().await?; +// println!("Result: {:?}", r); +// +// +// println!("Begin LISTEN..."); +// +// let bit_topic = pubsub::channel_bits::ChannelBitsEventsV2 { +// channel_id: user_id, +// }.into_topic(); +// +// let topics = [bit_topic]; +// +// client.listen(&auth_token, &topics).await?; +// +// println!("Try read next..."); +// let r = client.try_next().await?; +// println!("Result: {:?}", r); +// +// /* +// Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 { +// channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: +// Some(BadgeEntitlement { new_version: 100, previous_version: 1 }), bits_used: 100, +// channel_id: "652567283", channel_name: "vocodes", chat_message: "Cheer100 testing the cheer", +// context: Cheer, is_anonymous: false, time: "2021-09-27T04:30:53.627717085Z", +// total_bits_used: 101, user_id: "650154491", user_name: "testytest512" }, +// message_id: "793f745e-8f3e-5f71-bc41-4808c4b49a53", version: "1.0", is_anonymous: false } } }) +// +// Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 { +// channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: None, +// bits_used: 1, channel_id: "652567283", channel_name: "vocodes", +// chat_message: "mario: hello everybody Cheer1", context: Cheer, is_anonymous: false, +// time: "2021-09-27T04:47:32.943872952Z", total_bits_used: 102, user_id: "650154491", +// user_name: "testytest512" }, message_id: "5844328d-495b-5585-9d6c-3c99949f9a0a", version: "1.0", +// is_anonymous: false } } }) +// */ +// +// +// +// println!("Try read next..."); +// let r = client.try_next().await?; +// println!("Result: {:?}", r); +// +// println!("Sleep..."); +// std::thread::sleep(Duration::from_millis(30000)); +// +// //let text = msg.into_text()?; +// //println!("Text: {}", text); +// +// // ======================================================== // info!("Connecting to database..."); // @@ -285,6 +270,11 @@ async fn main() -> AnyhowResult<()> { cookie_http_only, website_homepage_redirect, }, + twitch_oauth_secrets: TwitchOauthSecrets { + client_id: secrets.app_client_id.clone(), + client_secret: secrets.app_client_secret.clone(), + redirect_url: "http://localhost:54321/twitch/oauth_redirect".to_string() + }, hostname: server_hostname, }; @@ -319,22 +309,19 @@ pub async fn serve(server_state: ObsGatewayServerState) -> AnyhowResult<()> .route(web::get().to(get_root_index)) ) // Twitch - .service( - web::resource("/twitch") - .route(web::get().to(ws_index)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - // /twitch_oauth_enroll - // /twitch_oauth_redirect - .service( - web::resource("/twitch_oauth_enroll") - .route(web::get().to(oauth_begin_enroll)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - .service( - web::resource("/twitch_oauth_redirect") - .route(web::get().to(oauth_begin_enroll)) - .route(web::head().to(|| HttpResponse::Ok())) + .service(web::scope("/twitch") + .service(web::resource("/oauth_enroll") + .route(web::get().to(oauth_begin_enroll)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/oauth_redirect") + .route(web::get().to(oauth_begin_enroll)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/websocket") + .route(web::get().to(ws_index)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) //.default_service( web::route().to(default_route_404)) }) diff --git a/crates/app/obs_gateway/src/server_state.rs b/crates/app/obs_gateway/src/server_state.rs new file mode 100644 index 0000000000..590f44be78 --- /dev/null +++ b/crates/app/obs_gateway/src/server_state.rs @@ -0,0 +1,31 @@ +use twitch_oauth2::{ClientId, ClientSecret}; + +/// State that is injected into every endpoint. +#[derive(Clone)] +pub struct ObsGatewayServerState { + /// Configuration from ENV vars. + /// Some of this might not be used. + pub env_config: EnvConfig, + pub hostname: String, + pub twitch_oauth_secrets: TwitchOauthSecrets, +} + +// TODO: Many of these do not need to be passed around past server init. +#[derive(Clone)] +pub struct EnvConfig { + // Number of thread workers. + pub num_workers: usize, + pub bind_address: String, + pub cookie_domain: String, + pub cookie_secure: bool, + pub cookie_http_only: bool, + pub website_homepage_redirect: String, +} + +/// Necessary to run the OAuth flow. +#[derive(Clone)] +pub struct TwitchOauthSecrets { + pub client_id: String, + pub client_secret: String, + pub redirect_url: String, +} diff --git a/crates/lib/http_server_common/Cargo.toml b/crates/lib/http_server_common/Cargo.toml index 1dc4b28a0c..d90d35fc76 100644 --- a/crates/lib/http_server_common/Cargo.toml +++ b/crates/lib/http_server_common/Cargo.toml @@ -14,6 +14,9 @@ path = "src/lib.rs" [dependencies] +# Internal +container_common = { path = "../container_common" } + # External anyhow = "1.0.40" base64 = "0.13.0" diff --git a/crates/lib/http_server_common/src/response/mod.rs b/crates/lib/http_server_common/src/response/mod.rs index 31ca5b7882..6bcb009c18 100644 --- a/crates/lib/http_server_common/src/response/mod.rs +++ b/crates/lib/http_server_common/src/response/mod.rs @@ -1,3 +1,4 @@ pub mod response_error_helpers; pub mod response_success_helpers; pub mod serialize_as_json_error; +pub mod to_json_success_response; diff --git a/crates/lib/http_server_common/src/response/to_json_success_response.rs b/crates/lib/http_server_common/src/response/to_json_success_response.rs new file mode 100644 index 0000000000..6d981a9d1b --- /dev/null +++ b/crates/lib/http_server_common/src/response/to_json_success_response.rs @@ -0,0 +1,11 @@ +use serde::Serialize; +use actix_web::HttpResponse; +use container_common::anyhow_result::AnyhowResult; + +pub fn to_json_success_response(response: &impl Serialize) -> AnyhowResult { + let body = serde_json::to_string(&response)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +}