Skip to content

Commit b8a7a69

Browse files
committed
Add initial code version
1 parent dc9d2fa commit b8a7a69

File tree

8 files changed

+269
-0
lines changed

8 files changed

+269
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/target
2+
**/*.rs.bk
3+
wasm-pack.log
4+
build/
5+
.idea/
6+
Cargo.lock

Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "obs-oauth-cf"
3+
version = "0.1.0"
4+
authors = ["dennis <[email protected]>"]
5+
edition = "2018"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[features]
11+
default = ["console_error_panic_hook"]
12+
13+
[dependencies]
14+
cfg-if = "0.1.2"
15+
worker = "0.0.9"
16+
serde_qs = "0.9.1"
17+
serde_json = "1.0.67"
18+
serde = "1.0.136"
19+
rand = "0.8.5"
20+
getrandom = {version = "0.2.6", features = ["js"]}
21+
reqwest = { version="0.11", features=["json"] }
22+
23+
# The `console_error_panic_hook` crate provides better debugging of panics by
24+
# logging them with `console.error`. This is great for development, but requires
25+
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
26+
# code size when deploying.
27+
console_error_panic_hook = { version = "0.1.1", optional = true }
28+
29+
[profile.release]
30+
# Tell `rustc` to optimize for small code size.
31+
opt-level = "s"

src/lib.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use worker::*;
2+
3+
mod utils;
4+
mod platforms;
5+
6+
fn log_request(req: &Request) {
7+
console_log!(
8+
"{} - [{}], located at: {:?}, within: {}",
9+
Date::now().to_string(),
10+
req.path(),
11+
req.cf().coordinates().unwrap_or_default(),
12+
req.cf().region().unwrap_or("unknown region".into())
13+
);
14+
}
15+
16+
const BLANK_PAGE: &str = "This is an open field west of a white house, with a boarded front door.
17+
There is a small mailbox here.
18+
>";
19+
const OAUTH_FINISHED : &str = "OAuth finished. This window should close momentarily.";
20+
21+
fn handle_redirects(_: Request, ctx: RouteContext<()>) -> Result<Response> {
22+
let provider = ctx.param("platform");
23+
let mut url: Option<String> = None;
24+
25+
match provider.unwrap().as_str() {
26+
"twitch" => url = Some(platforms::twitch::get_redirect_url(&ctx)),
27+
_ => {}
28+
}
29+
30+
if url.is_some() {
31+
Response::redirect(Url::parse(&url.unwrap()).unwrap())
32+
} else {
33+
Response::error(format!("Unknown provider: {}", provider.unwrap()), 404)
34+
}
35+
}
36+
37+
async fn handle_token(mut req: Request, ctx: RouteContext<()>) -> Result<Response> {
38+
let provider = ctx.param("platform");
39+
let params = req.form_data().await?;
40+
41+
match provider.unwrap().as_str() {
42+
"twitch" => platforms::twitch::get_token(params, &ctx).await,
43+
_ => Response::error(format!("Unknown provider: {}", provider.unwrap()), 404)
44+
}
45+
}
46+
47+
#[event(fetch)]
48+
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
49+
log_request(&req);
50+
utils::set_panic_hook();
51+
let router = Router::new();
52+
router
53+
.get("/", |_, _| Response::ok(BLANK_PAGE))
54+
.get("/v1/:platform/redirect", handle_redirects)
55+
.get("/v1/:platform/finished", |_, _| Response::ok(OAUTH_FINISHED))
56+
.post_async("/v1/:platform/token", |req, ctx| async move {
57+
let res = handle_token(req, ctx).await;
58+
if let Err(_res) = res {
59+
Response::error("Bad Request (Fuck)", 400)
60+
} else {
61+
res
62+
}
63+
})
64+
.run(req, env)
65+
.await
66+
}

src/platforms/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod twitch;
2+
mod utils;

src/platforms/twitch.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::collections::HashMap;
2+
3+
use reqwest;
4+
use serde::Serialize;
5+
use serde_json;
6+
use serde_qs;
7+
use worker::{FormData, FormEntry, Response, Result, RouteContext};
8+
9+
use crate::platforms::utils::generate_state_string;
10+
11+
const SCOPES: &str = "channel:read:stream_key";
12+
const TWITCH_AUTH_URL: &str = "https://id.twitch.tv/oauth2/authorize";
13+
const TWITCH_TOKEN_URL: &str = "https://id.twitch.tv/oauth2/token";
14+
15+
#[derive(Serialize)]
16+
struct RedirectParams {
17+
client_id: String,
18+
redirect_uri: String,
19+
response_type: String,
20+
scope: String,
21+
state: String,
22+
}
23+
24+
pub fn get_redirect_url(ctx: &RouteContext<()>) -> String {
25+
let q = RedirectParams {
26+
client_id: ctx.secret("TWITCH_ID").unwrap().to_string(),
27+
redirect_uri: ctx.secret("TWITCH_REDIRECT_URL").unwrap().to_string(),
28+
response_type: "code".to_string(),
29+
scope: SCOPES.to_string(),
30+
state: generate_state_string(),
31+
};
32+
33+
format!("{}?{}", TWITCH_AUTH_URL, serde_qs::to_string(&q).unwrap())
34+
}
35+
36+
fn get_param_val(form_data: &FormData, name: &str) -> Option<String> {
37+
// This is fucking atrocious.
38+
if let Some(value) = form_data.get(name) {
39+
if let FormEntry::Field(val) = value {
40+
return Some(val);
41+
}
42+
};
43+
44+
None
45+
}
46+
47+
pub async fn get_token(params: FormData, ctx: &RouteContext<()>) -> Result<Response> {
48+
let grant_type: String = get_param_val(&params, "grant_type").unwrap_or_default();
49+
let mut post_data: HashMap<&str, String> = HashMap::from([
50+
("client_id", ctx.secret("TWITCH_ID").unwrap().to_string()),
51+
("client_secret", ctx.secret("TWITCH_SECRET").unwrap().to_string()),
52+
("grant_type", grant_type.to_string())
53+
]);
54+
55+
match grant_type.as_str() {
56+
"refresh_token" => {
57+
post_data.insert("refresh_token", get_param_val(&params, "refresh_token").unwrap());
58+
}
59+
"authorization_code" => {
60+
post_data.insert("code", get_param_val(&params, "code").unwrap());
61+
post_data.insert("redirect_uri", ctx.secret("TWITCH_REDIRECT_URL").unwrap().to_string());
62+
}
63+
_ => return Response::error(format!("Invalid grant_type '{}'", grant_type), 400),
64+
}
65+
66+
let client = reqwest::Client::new();
67+
let _resp = client.post(TWITCH_TOKEN_URL).form(&post_data).send().await;
68+
69+
if _resp.is_err() {
70+
let resp = Response::from_json(&serde_json::json!({
71+
"error": "curl_error",
72+
"error_description": format!("Request failed with {}", _resp.err().unwrap())
73+
}))?;
74+
return Ok(resp.with_status(500));
75+
}
76+
let resp = _resp.unwrap();
77+
78+
let status = resp.status().as_u16();
79+
let _json = resp.json::<HashMap<String, serde_json::Value>>().await;
80+
if _json.is_err() {
81+
let res = Response::from_json(&serde_json::json!({
82+
"error": "parse_error",
83+
"error_description": "Bad JSON response from Twitch"
84+
}))?;
85+
return Ok(res.with_status(500));
86+
}
87+
let data = _json.unwrap();
88+
89+
90+
if status != 200 {
91+
let resp_data: serde_json::Value;
92+
93+
if data.contains_key("message") {
94+
if data["message"].as_str().unwrap() == "Invalid refresh token" {
95+
resp_data = serde_json::json!({
96+
"error": "Error",
97+
"error_description": "Your Twitch login token is no longer valid. Please try reconnecting your account."
98+
});
99+
} else {
100+
resp_data = serde_json::json!({
101+
"error": "Error",
102+
"error_description": data["message"].as_str().unwrap()
103+
})
104+
};
105+
} else {
106+
resp_data = serde_json::json!({
107+
"error": "status_error",
108+
"error_description": format!("Received HTTP {} from Twitch", status)
109+
});
110+
}
111+
let res = Response::from_json(&resp_data)?;
112+
return Ok(res.with_status(status));
113+
}
114+
115+
let res = Response::from_json(&data)?;
116+
if data.contains_key("error") {
117+
return Ok(res.with_status(500));
118+
}
119+
120+
Ok(res)
121+
}

src/platforms/utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use rand::distributions::Alphanumeric;
2+
use rand::Rng;
3+
4+
pub fn generate_state_string() -> String {
5+
let rand_string: String = rand::thread_rng()
6+
.sample_iter(&Alphanumeric)
7+
.take(32)
8+
.map(char::from)
9+
.collect();
10+
11+
rand_string
12+
}

src/utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use cfg_if::cfg_if;
2+
3+
cfg_if! {
4+
// https://github.com/rustwasm/console_error_panic_hook#readme
5+
if #[cfg(feature = "console_error_panic_hook")] {
6+
extern crate console_error_panic_hook;
7+
pub use self::console_error_panic_hook::set_once as set_panic_hook;
8+
} else {
9+
#[inline]
10+
pub fn set_panic_hook() {}
11+
}
12+
}

wrangler.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name = "obs-oauth-cf"
2+
type = "javascript"
3+
workers_dev = true
4+
compatibility_date = "2022-03-29"
5+
6+
[build]
7+
command = "cargo install -q worker-build && worker-build --release" # required
8+
9+
[build.upload]
10+
dir = "build/worker"
11+
format = "modules"
12+
main = "./shim.mjs"
13+
14+
[[build.upload.rules]]
15+
globs = ["**/*.wasm"]
16+
type = "CompiledWasm"
17+
18+
# read more about configuring your Worker via wrangler.toml at:
19+
# https://developers.cloudflare.com/workers/cli-wrangler/configuration

0 commit comments

Comments
 (0)