Fetch User Keys - simple tool for fetching SSH keys from various sources

ft: pop errors to main

hauleth.dev 45687921 8cbec9cf

verified
Changed files
+66 -47
cli
+10 -7
cli/src/config.rs
···
use crate::sources::*;
use rayon::prelude::*;
#[derive(Debug, serde::Deserialize)]
pub struct Entry {
···
}
impl Entry {
-
pub fn fetch(&self) -> (String, Vec<ssh_key::PublicKey>) {
-
let mut stream: Vec<_> = self.keys.par_iter().map(|k| k.fetch()).flatten().collect();
// Deduplicate keys, no need for duplicated entries
stream.sort();
stream.dedup_by(|a, b| a.key_data() == b.key_data());
-
(self.name.clone(), stream)
}
}
···
}
impl Config {
-
pub fn fetch(&self) -> Result<Output, ()> {
-
let keys = self.entries.into_par_iter().map(Entry::fetch).collect();
-
-
Ok(Output { keys })
}
}
···
use crate::sources::*;
use rayon::prelude::*;
+
use simple_eyre::eyre::Result;
#[derive(Debug, serde::Deserialize)]
pub struct Entry {
···
}
impl Entry {
+
pub fn fetch(&self) -> Result<(String, Vec<ssh_key::PublicKey>)> {
+
let mut stream: Vec<ssh_key::PublicKey> = self
+
.keys
+
.par_iter()
+
.flat_map(|k| k.fetch().unwrap())
+
.collect();
// Deduplicate keys, no need for duplicated entries
stream.sort();
stream.dedup_by(|a, b| a.key_data() == b.key_data());
+
Ok((self.name.clone(), stream))
}
}
···
}
impl Config {
+
pub fn fetch(&self) -> Result<Output> {
+
self.entries.into_par_iter().map(Entry::fetch).collect()
}
}
+14 -1
cli/src/output/mod.rs
···
use std::collections::HashMap;
use std::io::{self, prelude::*};
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Format {
JSON,
···
match self {
Format::JSON => {
serde_json::to_writer_pretty(&mut *w, &output.keys).map_err(io::Error::other)?;
-
writeln!(w, "")
}
Format::TOML => write!(w, "{}", toml::to_string_pretty(&output.keys).unwrap()),
Format::CSV => as_csv(w, output),
···
#[derive(Debug, serde::Serialize)]
pub struct Output {
pub keys: HashMap<String, Vec<ssh_key::PublicKey>>,
}
// TODO: proper escaping
···
use std::collections::HashMap;
use std::io::{self, prelude::*};
+
use rayon::prelude::*;
+
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Format {
JSON,
···
match self {
Format::JSON => {
serde_json::to_writer_pretty(&mut *w, &output.keys).map_err(io::Error::other)?;
+
writeln!(w)
}
Format::TOML => write!(w, "{}", toml::to_string_pretty(&output.keys).unwrap()),
Format::CSV => as_csv(w, output),
···
#[derive(Debug, serde::Serialize)]
pub struct Output {
pub keys: HashMap<String, Vec<ssh_key::PublicKey>>,
+
}
+
+
impl FromParallelIterator<(String, Vec<ssh_key::PublicKey>)> for Output {
+
fn from_par_iter<T>(iter: T) -> Self
+
where
+
T: IntoParallelIterator<Item = (String, Vec<ssh_key::PublicKey>)>,
+
{
+
Output {
+
keys: iter.into_par_iter().collect(),
+
}
+
}
}
// TODO: proper escaping
+27 -22
cli/src/sources/atproto.rs
···
use super::helpers;
use serde::Deserialize;
use ssh_key::PublicKey;
#[derive(Debug)]
-
pub struct DID {
method: String,
id: String,
}
-
impl FromStr for DID {
type Err = ();
fn from_str(input: &str) -> Result<Self, ()> {
···
return Err(());
}
-
Ok(DID {
method: chunks[1].into(),
id: chunks[2].into(),
})
}
}
-
impl fmt::Display for DID {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "did:{}:{}", self.method, self.id)
}
···
return Err(());
}
-
if (b'0'..=b'9').contains(&segments.last().unwrap().as_bytes()[0]) {
return Err(());
}
···
#[derive(Debug)]
pub enum Identifier {
-
DID(DID),
Handle(Handle),
}
···
fn from_str(input: &str) -> Result<Self, Self::Err> {
input
.parse()
-
.map(Identifier::DID)
.or_else(|_| input.parse().map(Identifier::Handle))
.map_err(|_| InvalidHandle(input.into()))
}
···
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
-
Identifier::DID(ref did) => write!(f, "{}", did),
Identifier::Handle(ref handle) => write!(f, "{}", handle),
}
}
···
fn legal_segment(segment: &str) -> bool {
let bytes = segment.as_bytes();
-
segment != ""
-
&& bytes.into_iter().all(|&b| allowed_byte(b))
&& *bytes.first().unwrap() != b'-'
&& *bytes.last().unwrap() != b'-'
}
fn allowed_byte(c: u8) -> bool {
-
(b'0'..=b'9').contains(&c) || (b'a'..=b'z').contains(&c) || c == b'-'
}
fn default_atproto() -> String {
···
key: String,
}
-
impl Into<PublicKey> for &Record {
-
fn into(self) -> PublicKey {
-
PublicKey::from_openssh(&self.value.key).unwrap()
}
}
}
impl super::Fetch for ATProto {
-
fn fetch(&self) -> Vec<PublicKey> {
-
let mut url = url::Url::parse(&self.host).unwrap();
url.query_pairs_mut()
.append_pair("repo", &self.handle.to_string())
···
url.set_path("xrpc/com.atproto.repo.listRecords");
let data = ureq::get(&url.to_string())
-
.call()
-
.unwrap()
.body_mut()
-
.read_to_string()
-
.unwrap();
-
let decoded: resp::Resp = serde_json::from_str(&data).unwrap();
-
decoded.records.iter().map(Into::into).collect()
}
}
···
use super::helpers;
use serde::Deserialize;
+
use simple_eyre::eyre::Result;
use ssh_key::PublicKey;
#[derive(Debug)]
+
pub struct Did {
method: String,
id: String,
}
+
impl FromStr for Did {
type Err = ();
fn from_str(input: &str) -> Result<Self, ()> {
···
return Err(());
}
+
Ok(Did {
method: chunks[1].into(),
id: chunks[2].into(),
})
}
}
+
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "did:{}:{}", self.method, self.id)
}
···
return Err(());
}
+
if segments.last().unwrap().as_bytes()[0].is_ascii_digit() {
return Err(());
}
···
#[derive(Debug)]
pub enum Identifier {
+
Did(Did),
Handle(Handle),
}
···
fn from_str(input: &str) -> Result<Self, Self::Err> {
input
.parse()
+
.map(Identifier::Did)
.or_else(|_| input.parse().map(Identifier::Handle))
.map_err(|_| InvalidHandle(input.into()))
}
···
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
+
Identifier::Did(ref did) => write!(f, "{}", did),
Identifier::Handle(ref handle) => write!(f, "{}", handle),
}
}
···
fn legal_segment(segment: &str) -> bool {
let bytes = segment.as_bytes();
+
!segment.is_empty()
+
&& bytes.iter().all(|&b| allowed_byte(b))
&& *bytes.first().unwrap() != b'-'
&& *bytes.last().unwrap() != b'-'
}
fn allowed_byte(c: u8) -> bool {
+
c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'-'
}
fn default_atproto() -> String {
···
key: String,
}
+
impl TryFrom<&Record> for PublicKey {
+
type Error = ssh_key::Error;
+
+
fn try_from(val: &Record) -> ssh_key::Result<PublicKey> {
+
PublicKey::from_openssh(&val.value.key)
}
}
}
impl super::Fetch for ATProto {
+
fn fetch(&self) -> Result<Vec<PublicKey>> {
+
let mut url = url::Url::parse(&self.host)?;
url.query_pairs_mut()
.append_pair("repo", &self.handle.to_string())
···
url.set_path("xrpc/com.atproto.repo.listRecords");
let data = ureq::get(&url.to_string())
+
.call()?
.body_mut()
+
.read_to_string()?;
+
let decoded: resp::Resp = serde_json::from_str(&data)?;
+
decoded
+
.records
+
.iter()
+
.map(|val| val.try_into().map_err(Into::into))
+
.collect()
}
}
+2 -2
cli/src/sources/helpers.rs
···
use std::fmt;
use std::str::FromStr;
-
use serde::{de, Deserialize, Deserializer};
pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
···
{
struct DeFromStr<T>(std::marker::PhantomData<T>);
-
impl<'de, T> de::Visitor<'de> for DeFromStr<T>
where
T: FromStr,
<T as FromStr>::Err: fmt::Display,
···
use std::fmt;
use std::str::FromStr;
+
use serde::{Deserialize, Deserializer, de};
pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
···
{
struct DeFromStr<T>(std::marker::PhantomData<T>);
+
impl<T> de::Visitor<'_> for DeFromStr<T>
where
T: FromStr,
<T as FromStr>::Err: fmt::Display,
+13 -15
cli/src/sources/mod.rs
···
// SPDX-License-Identifier: EUPL-1.2
use serde::Deserialize;
use ssh_key::PublicKey;
use std::process::Command;
···
pub use atproto::ATProto;
pub trait Fetch: std::fmt::Debug {
-
fn fetch(&self) -> Vec<PublicKey>;
}
#[derive(Debug, Deserialize)]
···
}
impl Fetch for Source {
-
fn fetch(&self) -> Vec<PublicKey> {
match *self {
Source::Raw(ref raw) => raw.fetch(),
Source::Hosts(ref raw) => raw.fetch(),
···
}
}
-
fn normalize_sourcehut<'a>(s: &'a str) -> std::borrow::Cow<'a, str> {
if s.starts_with("~") {
s.into()
} else {
···
pub struct Raw(Box<[PublicKey]>);
impl Fetch for Raw {
-
fn fetch(&self) -> Vec<PublicKey> {
-
self.0.clone().into()
}
}
···
pub struct Hosts(pub Box<[String]>);
impl Fetch for Hosts {
-
fn fetch(&self) -> Vec<PublicKey> {
// TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan`
let result = Command::new("ssh-keyscan").args(&self.0).output().unwrap();
-
std::str::from_utf8(&result.stdout)
-
.unwrap()
.trim()
.split('\n')
.map(str::trim)
···
// Ignore first column as it contain hostname which is not
// needed there
.map(|line| line.split_once(' ').unwrap().1)
-
.map(|k| PublicKey::from_openssh(&k).unwrap())
.collect()
}
}
···
}
impl Fetch for Http {
-
fn fetch(&self) -> Vec<PublicKey> {
ureq::get(&self.url)
-
.call()
-
.unwrap()
.body_mut()
-
.read_to_string()
-
.unwrap()
.trim()
.split('\n')
-
.map(|s| PublicKey::from_openssh(s).unwrap())
.collect()
}
}
···
// SPDX-License-Identifier: EUPL-1.2
use serde::Deserialize;
+
use simple_eyre::eyre::Result;
use ssh_key::PublicKey;
use std::process::Command;
···
pub use atproto::ATProto;
pub trait Fetch: std::fmt::Debug {
+
fn fetch(&self) -> Result<Vec<PublicKey>>;
}
#[derive(Debug, Deserialize)]
···
}
impl Fetch for Source {
+
fn fetch(&self) -> Result<Vec<PublicKey>> {
match *self {
Source::Raw(ref raw) => raw.fetch(),
Source::Hosts(ref raw) => raw.fetch(),
···
}
}
+
fn normalize_sourcehut(s: &str) -> std::borrow::Cow<str> {
if s.starts_with("~") {
s.into()
} else {
···
pub struct Raw(Box<[PublicKey]>);
impl Fetch for Raw {
+
fn fetch(&self) -> Result<Vec<PublicKey>> {
+
Ok(self.0.clone().into())
}
}
···
pub struct Hosts(pub Box<[String]>);
impl Fetch for Hosts {
+
fn fetch(&self) -> Result<Vec<PublicKey>> {
// TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan`
let result = Command::new("ssh-keyscan").args(&self.0).output().unwrap();
+
std::str::from_utf8(&result.stdout)?
.trim()
.split('\n')
.map(str::trim)
···
// Ignore first column as it contain hostname which is not
// needed there
.map(|line| line.split_once(' ').unwrap().1)
+
.map(|k| PublicKey::from_openssh(k).map_err(Into::into))
.collect()
}
}
···
}
impl Fetch for Http {
+
fn fetch(&self) -> Result<Vec<PublicKey>> {
ureq::get(&self.url)
+
.call()?
.body_mut()
+
.read_to_string()?
.trim()
.split('\n')
+
.map(|s| PublicKey::from_openssh(s).map_err(Into::into))
.collect()
}
}