Initial commit

This commit is contained in:
JP Stringham
2026-03-06 14:55:28 -05:00
commit 1b7b5aa592
11 changed files with 2442 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1988
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "matrix-admin-rs"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.60", features = ["derive"] }
colored = "3.1.1"
enum-iterator = "2.3.0"
reqwest = { version = "0.13.2", features = ["blocking", "form"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
shlex = "1.3.0"

3
deactivate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"skip_erase": false
}

4
newuser.json Normal file
View File

@@ -0,0 +1,4 @@
{
"username": "jp",
"skip_homeserver_check": true
}

0
oauthlinks.json Normal file
View File

7
postbody.json Normal file
View File

@@ -0,0 +1,7 @@
{
"password": "whatever",
"admin": false,
"deactivated": false,
"user_type": null,
"locked": false
}

4
setpassword.json Normal file
View File

@@ -0,0 +1,4 @@
{
"password": "1wshreb",
"skip_password_check": true
}

210
src/app.rs Normal file
View File

@@ -0,0 +1,210 @@
use std::time::{Duration, Instant};
use colored::{ColoredString, Colorize};
use reqwest::{StatusCode, Url};
use crate::json_types::*;
const MAS_SERVER_BASE_URL: &str = "https://mas.supersaturn.space";
const USERS_ENDPT: &str = "/api/admin/v1/users";
const OAUTH_LINKS_ENDPT: &str = "/api/admin/v1/upstream-oauth-links";
#[derive(Debug)]
pub struct App {
matrix_base_url: String,
mas_base_url: String,
auth_metadata: Option<AuthMetadata>,
client_registration: Option<ClientRegistration>,
auth_token: Option<AuthorizationToken>,
}
impl App {
pub fn new(matrix_base_url: &str, mas_base_url: &str) -> Self {
Self {
matrix_base_url: matrix_base_url.to_owned(),
mas_base_url: mas_base_url.to_owned(),
auth_metadata: None,
client_registration: None,
auth_token: None,
}
}
pub fn print_status(&self) {
println!("\nApp Base URL: {}", self.matrix_base_url);
println!(
"Auth Metadata? {}",
quick_format_bool(self.auth_metadata.is_some())
);
println!(
"Client Reg? {}",
quick_format_bool(self.client_registration.is_some())
);
}
pub fn get_auth_metadata(&mut self) {
const METADATA_ENDPT: &str = "/_matrix/client/unstable/org.matrix.msc2965/auth_metadata";
let client = reqwest::blocking::Client::new();
let req = client
.get(format!("{}{METADATA_ENDPT}", self.matrix_base_url))
.header("Accept", "application/json");
let res = req.send().unwrap();
let Ok(metadata) = serde_json::from_str::<AuthMetadata>(&res.text().unwrap()) else {
panic!("Could not get metadata...");
};
// println!("{}", serde_json::to_string_pretty(&metadata).unwrap());
self.auth_metadata = Some(metadata);
}
pub fn register_client(&mut self) {
let auth_metadata = self
.auth_metadata
.as_ref()
.expect("Can't register client without getting auth metadata");
let crr = ClientRegisterReq {
client_name: "JP CLI Tool".to_owned(),
client_uri: "https://myclitool.supersaturn.space/".to_owned(),
grant_types: vec![
"urn:ietf:params:oauth:grant-type:device_code".to_owned(),
"refresh_token".to_owned(),
],
application_type: "native".to_owned(),
token_endpoint_auth_method: "none".to_owned(),
};
let json = serde_json::to_string(&crr).unwrap();
let client = reqwest::blocking::Client::new();
let req = client
.post(auth_metadata.registration_endpoint.as_str())
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.body(json);
let res = req.send().unwrap();
let Ok(crres) = serde_json::from_str::<ClientRegistration>(&res.text().unwrap()) else {
panic!("Could not get metadata...");
};
// println!("{}", serde_json::to_string_pretty(&crres).unwrap());
self.client_registration = Some(crres);
}
pub fn authorize_device(&mut self) {
let auth_metadata = self
.auth_metadata
.as_ref()
.expect("Can't authorize device without auth metadata");
let client_registration = self
.client_registration
.as_ref()
.expect("Can't authorize device without client registration");
let client = reqwest::blocking::Client::new();
let req = client
.post(&auth_metadata.device_authorization_endpoint)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.form(&[
("client_id", client_registration.client_id.as_str()),
(
"scope",
"urn:mas:admin urn:synapse:admin:* urn:matrix:client:api:*",
),
]);
// println!("Req: {:?}\n\n", req);
let res = req.send().unwrap();
let Ok(device_grant) = serde_json::from_str::<DeviceGrant>(&res.text().unwrap()) else {
panic!("Could not get device grant")
};
println!("Device Grant: {:?}", device_grant);
let start = Instant::now();
let expiry = Duration::from_secs(device_grant.expires_in);
let interval = Duration::from_secs(device_grant.interval);
loop {
std::thread::sleep(interval);
if start.elapsed() > expiry {
println!("Request expired!");
break;
}
let req = client
.post(&auth_metadata.token_endpoint)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.form(&[
("client_id", client_registration.client_id.as_str()),
("device_code", device_grant.device_code.as_str()),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
]);
let res = req.send().unwrap();
let status = res.status();
let res_text = res.text().unwrap();
match status {
StatusCode::OK => {
let token = serde_json::from_str::<AuthorizationToken>(&res_text)
.expect("Couldn't decode auth token");
println!("{:?}", token);
println!("Authorized!");
self.auth_token = Some(token);
break;
}
_ => {
println!("...\n\n{}\n\n...", res_text);
println!("Waiting for auth...");
}
}
}
}
pub fn show_oath_links(&self) {
let auth_token = self
.auth_token
.as_ref()
.expect("Need auth token to perform this action");
let url = format!("{}{OAUTH_LINKS_ENDPT}", self.mas_base_url);
let client = reqwest::blocking::Client::new();
let req = client
.get(url)
.header("Accept", "application/json")
.header(
"Authorization",
format!("Bearer {}", auth_token.access_token.clone()),
)
.header("Content-Type", "application/json")
.body(include_str!("../oauthlinks.json"));
println!("Req {:?}", req);
let res = req.send().unwrap();
println!("{}", res.text().unwrap());
}
}
fn quick_format_bool(b: bool) -> ColoredString {
match b {
true => "true".bright_green(),
false => "false".red(),
}
}

55
src/json_types.rs Normal file
View File

@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ClientRegistration {
pub client_id: String,
pub client_id_issued_at: u64,
pub grant_types: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ClientRegisterReq {
pub client_name: String,
pub client_uri: String,
pub grant_types: Vec<String>,
pub application_type: String,
pub token_endpoint_auth_method: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct AuthMetadata {
pub device_authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct DeviceGrant {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct AuthorizationToken {
pub token_type: String,
pub access_token: String,
pub refresh_token: String,
pub expires_in: u64,
pub scope: String,
}
#[allow(dead_code)]
pub fn prettify_json_str(json_str: &str) -> Result<String, ()> {
let Ok(json) = serde_json::from_str::<serde_json::value::Value>(json_str) else {
return Err(());
};
match serde_json::to_string_pretty(&json) {
Ok(s) => Ok(s),
Err(_) => Err(()),
}
}

157
src/main.rs Normal file
View File

@@ -0,0 +1,157 @@
// these were for synapse, disabled with MAS
// const USERS_ENDPOINT_V3: &str = "/_synapse/admin/v3/users";
// const USERS_ENDPOINT_V2: &str = "/_synapse/admin/v2/users";
// const REGISTRATION_TOKENS_ENDPOINT: &str = "/_synapse/admin/v1/registration_tokens";
// q25W9kalf3EvMvJbLCkxByXym7OukM1F
//01KJZ6KYHQRRPMG3M49CZSBTM4Z
//01KJZ4RN8HAXYMHBC7F7H608EP
mod app;
pub mod json_types;
use json_types::*;
use std::io::Write;
use clap::{CommandFactory, Parser, Subcommand};
use colored::Colorize;
use enum_iterator::Sequence;
use crate::app::App;
// google oauth
// 754241279547-kk48of2t6eu5vol85hn6op3ofpp3em76.apps.googleusercontent.com
const MATRIX_SERVER_BASE_URL: &str = "https://matrix.supersaturn.space";
const MAS_SERVER_BASE_URL: &str = "https://mas.supersaturn.space";
const CLIENT_SECRET: &str = "asdjfja2392ijf23923ndflkas01812j312k3j_18127";
fn main() {
//curl --header "Authorization: Bearer syt_anA_YRbjQUlvKVDuAfIDIdPW_0k2VVC" -X GET http://127.0.0.1:8008/_synapse/admin/v2/users/@jp:matrix.supersaturn.space
// let access_token = "Bearer mat_zA80YL1OafucuRGgdZSPIILCSquto2_nT9b73";
print!("\x1B[2J\x1B[2;5H");
println!("{}", "Welcome to auth tools CLI".bright_cyan());
println!("{} {MATRIX_SERVER_BASE_URL}\n\n", "Homeserver:".green());
print_menu();
let mut app = App::new(MATRIX_SERVER_BASE_URL, MAS_SERVER_BASE_URL);
app.get_auth_metadata();
app.register_client();
app.print_status();
loop {
let input = readline().unwrap();
let input = input.trim();
if input.is_empty() {
continue;
}
let Some(args) = shlex::split(input) else {
continue;
};
let cli = match Cli::try_parse_from(args) {
Ok(c) => c,
Err(e) => {
println!("{}", e.to_string());
continue;
}
};
match cli.command {
Commands::GetAuthMetadata => app.get_auth_metadata(),
Commands::RegisterClient => app.register_client(),
Commands::AuthorizeDevice => app.authorize_device(),
Commands::ShowOauthLinks => app.show_oath_links(),
}
}
//
// todo!("nope");
// let req = client
// .post(format!(
// "{base_url}{USERS_ENDPT}/01KJXZRB1FSNNQ22DDC0368V9G/deactivate"
// ))
// .header("Accept", "application/json")
// .header("Authorization", access_token)
// .header("Content-Type", "application/json")
// .body(include_str!("../deactivate.json"));
// let req = client
// .post(format!("{base_url}{REGISTRATION_TOKENS_ENDPOINT}/new"))
// .header("Authorization", format!("Bearer {access_token}"))
// .body("{}");
// let req = client.get(format!("{base_url}{USERS_ENDPOINT_V3}")).header(
// "Authorization",
// "Bearer syt_anA_YRbjQUlvKVDuAfIDIdPW_0k2VVC",
// );
// let req = client
// .put(format!(
// "{base_url}{USERS_ENDPOINT_V2}/@afriend:supersaturn.space"
// ))
// .header("Authorization", "))
// .body(include_str!("../postbody.json"));
// println!("{:?}", req);
// let req = req.send().unwrap();
// match req.status() {
// StatusCode::OK | StatusCode::NO_CONTENT | StatusCode::CREATED => (),
// StatusCode::UNAUTHORIZED => {
// if !req.text().unwrap().contains("token expired") {
// panic!("Not authorized: {}", req.text().unwrap());
// }
// let req = client.get(format!("{MAS_SERVER_BASE_URL}{TOKEN_ENDPT}"));
// }
// sc => panic!("Bad response?\n{}\n{:?}", sc, req.text()),
// };
// let req_txt = req.text().unwrap();
// // println!("{}", &req_txt);
// let txt = prettify_json_str(&req_txt);
// println!("Blocking req status\n\n{}\n\n", txt.unwrap());
//
}
const MENU_HELP_TEMPLATE: &str = "\
{about-with-newline}\n\
{all-args}{after-help}\
";
fn print_menu() {
let mut cli = Cli::command().help_template(MENU_HELP_TEMPLATE);
cli.print_long_help().unwrap();
}
fn readline() -> Result<String, String> {
write!(std::io::stdout(), "> ").map_err(|e| e.to_string())?;
std::io::stdout().flush().map_err(|e| e.to_string())?;
let mut buffer = String::new();
std::io::stdin()
.read_line(&mut buffer)
.map_err(|e| e.to_string())?;
Ok(buffer)
}
#[derive(Debug, clap::Parser)]
#[command(multicall = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand, PartialEq, Eq, Sequence)]
enum Commands {
GetAuthMetadata,
RegisterClient,
AuthorizeDevice,
ShowOauthLinks,
}