use anyhow::Context;
use clap::{Parser, Subcommand};
use moq_token::Algorithm;
use std::{io, path::PathBuf};
#[derive(Debug, Parser)]
#[command(name = "moq-token")]
#[command(about = "Generate, sign, and verify tokens for moq-relay", long_about = None)]
#[command(version = env!("VERSION"))]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Generate {
#[arg(long, default_value = "HS256")]
algorithm: Algorithm,
#[arg(long)]
id: Option<String>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long, conflicts_with = "out")]
out_dir: Option<PathBuf>,
#[arg(long)]
public: Option<PathBuf>,
#[arg(long, conflicts_with = "public")]
public_dir: Option<PathBuf>,
#[arg(long)]
base64: bool,
},
Sign {
#[arg(long)]
key: PathBuf,
#[arg(long, default_value = "")]
root: String,
#[arg(long)]
publish: Vec<String>,
#[arg(long)]
cluster: bool,
#[arg(long)]
subscribe: Vec<String>,
#[arg(long, value_parser = parse_unix_timestamp)]
expires: Option<std::time::SystemTime>,
#[arg(long, value_parser = parse_unix_timestamp)]
issued: Option<std::time::SystemTime>,
},
Verify {
#[arg(long)]
key: PathBuf,
},
}
fn write_key(key: &moq_token::Key, path: &std::path::Path, base64: bool) -> anyhow::Result<()> {
if base64 {
Ok(key.to_file_base64url(path)?)
} else {
Ok(key.to_file(path)?)
}
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Generate {
algorithm,
id,
out,
out_dir,
public,
public_dir,
base64,
} => {
let id = match id {
Some(id) => moq_token::KeyId::decode(&id)?,
None => moq_token::KeyId::random(),
};
let key = moq_token::Key::generate(algorithm, Some(id.clone()))?;
if let Some(dir) = public_dir {
let path = dir.join(format!("{id}.jwk"));
write_key(&key.to_public()?, &path, base64)?;
} else if let Some(path) = public {
write_key(&key.to_public()?, &path, base64)?;
}
if let Some(dir) = out_dir {
let path = dir.join(format!("{id}.jwk"));
write_key(&key, &path, base64)?;
} else if let Some(path) = out {
write_key(&key, &path, base64)?;
} else {
let json = key.to_str()?;
println!("{json}");
}
}
Commands::Sign {
key,
root,
publish,
cluster,
subscribe,
expires,
issued,
} => {
let key = moq_token::Key::from_file(key)?;
let payload = moq_token::Claims {
root,
publish,
cluster,
subscribe,
expires,
issued,
};
let token = key.encode(&payload)?;
println!("{token}");
}
Commands::Verify { key } => {
let key = moq_token::Key::from_file(key)?;
let token = io::read_to_string(io::stdin())?.trim().to_string();
let payload = key.decode(&token)?;
println!("{payload:#?}");
}
}
Ok(())
}
fn parse_unix_timestamp(s: &str) -> anyhow::Result<std::time::SystemTime> {
let timestamp = s.parse::<i64>().context("expected unix timestamp")?;
let timestamp = timestamp.try_into().context("timestamp out of range")?;
Ok(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(timestamp))
}