mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-02 01:01:09 +00:00
feat(blahctl): add identity validate
This commit is contained in:
parent
82ab7f7d2c
commit
023da5ecb2
1 changed files with 86 additions and 1 deletions
|
@ -19,8 +19,11 @@ use rusqlite::{named_params, Connection};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
/// NB. Sync with docs of [`User::url`].
|
/// NB. Sync with docs of [`User::url`].
|
||||||
|
// FIXME: Remove old interface.
|
||||||
const KEY_URL_SUBPATH: &str = "/.well-known/blah/key";
|
const KEY_URL_SUBPATH: &str = "/.well-known/blah/key";
|
||||||
|
|
||||||
|
const USER_AGENT: &str = concat!("blahctl/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
/// Control or manage Blah Chat Server.
|
/// Control or manage Blah Chat Server.
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
#[clap(about, version = option_env!("CFG_RELEASE").unwrap_or(env!("CARGO_PKG_VERSION")))]
|
#[clap(about, version = option_env!("CFG_RELEASE").unwrap_or(env!("CARGO_PKG_VERSION")))]
|
||||||
|
@ -77,6 +80,11 @@ enum IdCommand {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
id_url: IdUrl,
|
id_url: IdUrl,
|
||||||
},
|
},
|
||||||
|
/// Validate identity description from a JSON file or URL.
|
||||||
|
Validate {
|
||||||
|
#[command(flatten)]
|
||||||
|
id_desc_args: IdDescArgs,
|
||||||
|
},
|
||||||
/// Add an action subkey to an existing identity description.
|
/// Add an action subkey to an existing identity description.
|
||||||
AddActKey {
|
AddActKey {
|
||||||
/// The identity description JSON file to modify.
|
/// The identity description JSON file to modify.
|
||||||
|
@ -101,6 +109,65 @@ enum IdCommand {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, clap::Args)]
|
||||||
|
#[group(required = true, multiple = false)]
|
||||||
|
struct IdDescArgs {
|
||||||
|
/// The identity URL to check.
|
||||||
|
///
|
||||||
|
/// It should be a HTTPS domain with a top-level path `/`.
|
||||||
|
#[arg(long)]
|
||||||
|
id_url: Option<IdUrl>,
|
||||||
|
|
||||||
|
/// The identity description JSON path to check.
|
||||||
|
#[arg(long, short = 'f')]
|
||||||
|
desc_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdDescArgs {
|
||||||
|
fn load(&self, rt: &Runtime) -> Result<UserIdentityDesc> {
|
||||||
|
const LARGE_BODY_SIZE: usize = 64 << 10; // 64KiB
|
||||||
|
|
||||||
|
let text = if let Some(url) = &self.id_url {
|
||||||
|
if url.scheme() == "http" {
|
||||||
|
// TODO: Verbosity control.
|
||||||
|
eprintln!("warning: id_url has scheme http, which will be rejected by most server");
|
||||||
|
}
|
||||||
|
if url.port().is_some() {
|
||||||
|
eprintln!("warning: id_url has custom port, which will be rejected by most server");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = url
|
||||||
|
.join(UserIdentityDesc::WELL_KNOWN_PATH)
|
||||||
|
.expect("IdUrl must be a valid base");
|
||||||
|
rt.block_on(async {
|
||||||
|
anyhow::Ok(
|
||||||
|
build_client()?
|
||||||
|
.get(url.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.with_context(|| format!("failed to GET {url}"))?
|
||||||
|
} else if let Some(path) = &self.desc_file {
|
||||||
|
fs::read_to_string(path).context("failed to read from desc_file")?
|
||||||
|
} else {
|
||||||
|
unreachable!("enforced by clap");
|
||||||
|
};
|
||||||
|
|
||||||
|
if text.len() > LARGE_BODY_SIZE {
|
||||||
|
eprintln!(
|
||||||
|
"warning: large description size ({}KiB), which will be rejected by most server",
|
||||||
|
LARGE_BODY_SIZE >> 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_str(&text).context("failed to parse identity description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
#[derive(Debug, clap::Subcommand)]
|
||||||
enum DbCommand {
|
enum DbCommand {
|
||||||
/// Create and initialize database.
|
/// Create and initialize database.
|
||||||
|
@ -180,7 +247,13 @@ impl User {
|
||||||
fs::read_to_string(path).context("failed to read key file")?
|
fs::read_to_string(path).context("failed to read key file")?
|
||||||
} else if let Some(url) = &self.url {
|
} else if let Some(url) = &self.url {
|
||||||
let url = url.join(KEY_URL_SUBPATH)?;
|
let url = url.join(KEY_URL_SUBPATH)?;
|
||||||
reqwest::get(url).await?.error_for_status()?.text().await?
|
build_client()?
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
@ -221,6 +294,14 @@ fn build_rt() -> Result<Runtime> {
|
||||||
.context("failed to initialize tokio runtime")
|
.context("failed to initialize tokio runtime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_client() -> Result<reqwest::Client> {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()
|
||||||
|
.context("failed to build HTTP client")
|
||||||
|
}
|
||||||
|
|
||||||
fn main_id(cmd: IdCommand) -> Result<()> {
|
fn main_id(cmd: IdCommand) -> Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
IdCommand::Generate {
|
IdCommand::Generate {
|
||||||
|
@ -256,6 +337,10 @@ fn main_id(cmd: IdCommand) -> Result<()> {
|
||||||
.context("failed to save private key")?;
|
.context("failed to save private key")?;
|
||||||
fs::write(desc_file, &id_desc_str).context("failed to save identity description")?;
|
fs::write(desc_file, &id_desc_str).context("failed to save identity description")?;
|
||||||
}
|
}
|
||||||
|
IdCommand::Validate { id_desc_args } => {
|
||||||
|
let id_desc = id_desc_args.load(&build_rt()?)?;
|
||||||
|
id_desc.verify(id_desc_args.id_url.as_ref(), get_timestamp())?;
|
||||||
|
}
|
||||||
IdCommand::AddActKey {
|
IdCommand::AddActKey {
|
||||||
desc_file,
|
desc_file,
|
||||||
id_key_file,
|
id_key_file,
|
||||||
|
|
Loading…
Add table
Reference in a new issue