git_auth/
github.rs

1use crate::{Login, error::GithubError};
2use colored::Colorize;
3use serde_json::Value;
4use std::{
5    collections::HashMap,
6    thread,
7    time::{self, Duration},
8};
9
10pub fn get_login() -> Result<Login, GithubError> {
11    let client_id = String::from("Ov23liAXHnUzobAF9AuF");
12    let client = reqwest::blocking::Client::new();
13
14    let device_code = get_device_code(&client, &client_id)?;
15    let token = poll_for_auth(&client, &device_code, &client_id)?;
16    let username = query_username(&client, &token)?;
17    let email = query_email(&client, &token)?;
18
19    let login = Login::new(username, String::from("github.com"), email);
20    login.set_password(&token)?;
21    Ok(login)
22}
23
24fn get_device_code(
25    reqwest_client: &reqwest::blocking::Client,
26    client_id: &str,
27) -> Result<String, GithubError> {
28    let params = [
29        ("scope", "repo read:user user:email"),
30        ("client_id", client_id),
31    ];
32    let response: HashMap<String, Value> = reqwest_client
33        .post("https://github.com/login/device/code")
34        .header("Accept", "application/vnd.github+json ")
35        .form(&params)
36        .send()?
37        .json()?;
38
39    eprintln!(
40        "Copy this code <{}> and follow the instructions at the link\n\t{}",
41        response
42            .get("user_code")
43            .ok_or(GithubError::MissingField(String::from("user_code")))?
44            .as_str()
45            .ok_or(GithubError::InvalidField(String::from("user_code")))?
46            .green()
47            .bold(),
48        response
49            .get("verification_uri")
50            .ok_or(GithubError::MissingField(String::from("verification_uri")))?
51            .as_str()
52            .ok_or(GithubError::InvalidField(String::from("verification_uri")))?
53            .blue()
54            .underline()
55    );
56
57    // TODO: Add expiry reading here so timeout is based on expiry
58    Ok(response
59        .get("device_code")
60        .ok_or(GithubError::MissingField(String::from("device_code")))?
61        .as_str()
62        .ok_or(GithubError::InvalidField(String::from("device_code")))?
63        .to_string())
64}
65
66fn poll_for_auth(
67    reqwest_client: &reqwest::blocking::Client,
68    device_code: &str,
69    client_id: &str,
70) -> Result<String, GithubError> {
71    let poll_params = [
72        ("device_code", device_code),
73        ("client_id", client_id),
74        ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
75    ];
76    let loop_start = time::SystemTime::now();
77    loop {
78        let res: HashMap<String, Value> = reqwest_client
79            .post("https://github.com/login/oauth/access_token")
80            .header("Accept", "application/vnd.github+json")
81            .form(&poll_params)
82            .send()?
83            .json()?;
84        match res.get("access_token") {
85            Some(token) => {
86                break Ok(token
87                    .as_str()
88                    .ok_or(GithubError::InvalidField(String::from("access_token")))?
89                    .to_string());
90            }
91            None => {
92                // TODO: Read expiry from initial api response
93                if time::SystemTime::now()
94                    .duration_since(loop_start)
95                    .expect("now cannot be before loop_start")
96                    .as_secs()
97                    > 900
98                {
99                    eprintln!("Code has expired, exiting");
100                    break Err(GithubError::Timeout(900));
101                }
102                thread::sleep(Duration::from_secs(5));
103            }
104        }
105    }
106}
107
108fn query_username(
109    reqwest_client: &reqwest::blocking::Client,
110    token: &str,
111) -> Result<String, GithubError> {
112    let res: HashMap<String, Value> = reqwest_client
113        .get("https://api.github.com/user")
114        .header("User-Agent", "git-auth")
115        .header("Accept", "application/vnd.github+json")
116        .header("Authorization", format!("token {token}"))
117        .send()?
118        .json()?;
119    Ok(res
120        .get("login")
121        .ok_or(GithubError::MissingField(String::from("login")))?
122        .as_str()
123        .ok_or(GithubError::InvalidField(String::from("login")))?
124        .to_string())
125}
126
127fn query_email(
128    reqwest_client: &reqwest::blocking::Client,
129    token: &str,
130) -> Result<Option<String>, GithubError> {
131    let res: Vec<Value> = reqwest_client
132        .get("https://api.github.com/user/emails")
133        .header("User-Agent", "git-auth")
134        .header("Accept", "application/vnd.github+json")
135        .header("Authorization", format!("token {token}"))
136        .send()?
137        .json()?;
138    for email in res {
139        if let Some(Value::Bool(true)) = email.get("primary") {
140            return Ok(email
141                .get("email")
142                .and_then(|e| e.as_str().map(|e_str| e_str.to_string())));
143        }
144    }
145
146    Ok(None)
147}