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

Configure Feed

Select the types of activity you want to include in your feed.

ft: pop errors to main

+66 -47
+10 -7
cli/src/config.rs
··· 6 6 use crate::sources::*; 7 7 8 8 use rayon::prelude::*; 9 + use simple_eyre::eyre::Result; 9 10 10 11 #[derive(Debug, serde::Deserialize)] 11 12 pub struct Entry { ··· 14 15 } 15 16 16 17 impl Entry { 17 - pub fn fetch(&self) -> (String, Vec<ssh_key::PublicKey>) { 18 - let mut stream: Vec<_> = self.keys.par_iter().map(|k| k.fetch()).flatten().collect(); 18 + pub fn fetch(&self) -> Result<(String, Vec<ssh_key::PublicKey>)> { 19 + let mut stream: Vec<ssh_key::PublicKey> = self 20 + .keys 21 + .par_iter() 22 + .flat_map(|k| k.fetch().unwrap()) 23 + .collect(); 19 24 20 25 // Deduplicate keys, no need for duplicated entries 21 26 stream.sort(); 22 27 stream.dedup_by(|a, b| a.key_data() == b.key_data()); 23 28 24 - (self.name.clone(), stream) 29 + Ok((self.name.clone(), stream)) 25 30 } 26 31 } 27 32 ··· 32 37 } 33 38 34 39 impl Config { 35 - pub fn fetch(&self) -> Result<Output, ()> { 36 - let keys = self.entries.into_par_iter().map(Entry::fetch).collect(); 37 - 38 - Ok(Output { keys }) 40 + pub fn fetch(&self) -> Result<Output> { 41 + self.entries.into_par_iter().map(Entry::fetch).collect() 39 42 } 40 43 }
+14 -1
cli/src/output/mod.rs
··· 5 5 use std::collections::HashMap; 6 6 use std::io::{self, prelude::*}; 7 7 8 + use rayon::prelude::*; 9 + 8 10 #[derive(PartialEq, Eq, Debug, Copy, Clone)] 9 11 pub enum Format { 10 12 JSON, ··· 18 20 match self { 19 21 Format::JSON => { 20 22 serde_json::to_writer_pretty(&mut *w, &output.keys).map_err(io::Error::other)?; 21 - writeln!(w, "") 23 + writeln!(w) 22 24 } 23 25 Format::TOML => write!(w, "{}", toml::to_string_pretty(&output.keys).unwrap()), 24 26 Format::CSV => as_csv(w, output), ··· 53 55 #[derive(Debug, serde::Serialize)] 54 56 pub struct Output { 55 57 pub keys: HashMap<String, Vec<ssh_key::PublicKey>>, 58 + } 59 + 60 + impl FromParallelIterator<(String, Vec<ssh_key::PublicKey>)> for Output { 61 + fn from_par_iter<T>(iter: T) -> Self 62 + where 63 + T: IntoParallelIterator<Item = (String, Vec<ssh_key::PublicKey>)>, 64 + { 65 + Output { 66 + keys: iter.into_par_iter().collect(), 67 + } 68 + } 56 69 } 57 70 58 71 // TODO: proper escaping
+27 -22
cli/src/sources/atproto.rs
··· 8 8 use super::helpers; 9 9 10 10 use serde::Deserialize; 11 + use simple_eyre::eyre::Result; 11 12 use ssh_key::PublicKey; 12 13 13 14 #[derive(Debug)] 14 - pub struct DID { 15 + pub struct Did { 15 16 method: String, 16 17 id: String, 17 18 } 18 19 19 - impl FromStr for DID { 20 + impl FromStr for Did { 20 21 type Err = (); 21 22 22 23 fn from_str(input: &str) -> Result<Self, ()> { ··· 33 34 return Err(()); 34 35 } 35 36 36 - Ok(DID { 37 + Ok(Did { 37 38 method: chunks[1].into(), 38 39 id: chunks[2].into(), 39 40 }) 40 41 } 41 42 } 42 43 43 - impl fmt::Display for DID { 44 + impl fmt::Display for Did { 44 45 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 45 46 write!(f, "did:{}:{}", self.method, self.id) 46 47 } ··· 82 83 return Err(()); 83 84 } 84 85 85 - if (b'0'..=b'9').contains(&segments.last().unwrap().as_bytes()[0]) { 86 + if segments.last().unwrap().as_bytes()[0].is_ascii_digit() { 86 87 return Err(()); 87 88 } 88 89 ··· 100 101 101 102 #[derive(Debug)] 102 103 pub enum Identifier { 103 - DID(DID), 104 + Did(Did), 104 105 Handle(Handle), 105 106 } 106 107 ··· 110 111 fn from_str(input: &str) -> Result<Self, Self::Err> { 111 112 input 112 113 .parse() 113 - .map(Identifier::DID) 114 + .map(Identifier::Did) 114 115 .or_else(|_| input.parse().map(Identifier::Handle)) 115 116 .map_err(|_| InvalidHandle(input.into())) 116 117 } ··· 119 120 impl fmt::Display for Identifier { 120 121 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 121 122 match *self { 122 - Identifier::DID(ref did) => write!(f, "{}", did), 123 + Identifier::Did(ref did) => write!(f, "{}", did), 123 124 Identifier::Handle(ref handle) => write!(f, "{}", handle), 124 125 } 125 126 } ··· 146 147 147 148 fn legal_segment(segment: &str) -> bool { 148 149 let bytes = segment.as_bytes(); 149 - segment != "" 150 - && bytes.into_iter().all(|&b| allowed_byte(b)) 150 + !segment.is_empty() 151 + && bytes.iter().all(|&b| allowed_byte(b)) 151 152 && *bytes.first().unwrap() != b'-' 152 153 && *bytes.last().unwrap() != b'-' 153 154 } 154 155 155 156 fn allowed_byte(c: u8) -> bool { 156 - (b'0'..=b'9').contains(&c) || (b'a'..=b'z').contains(&c) || c == b'-' 157 + c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'-' 157 158 } 158 159 159 160 fn default_atproto() -> String { ··· 179 180 key: String, 180 181 } 181 182 182 - impl Into<PublicKey> for &Record { 183 - fn into(self) -> PublicKey { 184 - PublicKey::from_openssh(&self.value.key).unwrap() 183 + impl TryFrom<&Record> for PublicKey { 184 + type Error = ssh_key::Error; 185 + 186 + fn try_from(val: &Record) -> ssh_key::Result<PublicKey> { 187 + PublicKey::from_openssh(&val.value.key) 185 188 } 186 189 } 187 190 } 188 191 189 192 impl super::Fetch for ATProto { 190 - fn fetch(&self) -> Vec<PublicKey> { 191 - let mut url = url::Url::parse(&self.host).unwrap(); 193 + fn fetch(&self) -> Result<Vec<PublicKey>> { 194 + let mut url = url::Url::parse(&self.host)?; 192 195 193 196 url.query_pairs_mut() 194 197 .append_pair("repo", &self.handle.to_string()) ··· 197 200 url.set_path("xrpc/com.atproto.repo.listRecords"); 198 201 199 202 let data = ureq::get(&url.to_string()) 200 - .call() 201 - .unwrap() 203 + .call()? 202 204 .body_mut() 203 - .read_to_string() 204 - .unwrap(); 205 + .read_to_string()?; 205 206 206 - let decoded: resp::Resp = serde_json::from_str(&data).unwrap(); 207 + let decoded: resp::Resp = serde_json::from_str(&data)?; 207 208 208 - decoded.records.iter().map(Into::into).collect() 209 + decoded 210 + .records 211 + .iter() 212 + .map(|val| val.try_into().map_err(Into::into)) 213 + .collect() 209 214 } 210 215 }
+2 -2
cli/src/sources/helpers.rs
··· 5 5 use std::fmt; 6 6 use std::str::FromStr; 7 7 8 - use serde::{de, Deserialize, Deserializer}; 8 + use serde::{Deserialize, Deserializer, de}; 9 9 10 10 pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error> 11 11 where ··· 61 61 { 62 62 struct DeFromStr<T>(std::marker::PhantomData<T>); 63 63 64 - impl<'de, T> de::Visitor<'de> for DeFromStr<T> 64 + impl<T> de::Visitor<'_> for DeFromStr<T> 65 65 where 66 66 T: FromStr, 67 67 <T as FromStr>::Err: fmt::Display,
+13 -15
cli/src/sources/mod.rs
··· 4 4 // SPDX-License-Identifier: EUPL-1.2 5 5 6 6 use serde::Deserialize; 7 + use simple_eyre::eyre::Result; 7 8 use ssh_key::PublicKey; 8 9 use std::process::Command; 9 10 ··· 13 14 pub use atproto::ATProto; 14 15 15 16 pub trait Fetch: std::fmt::Debug { 16 - fn fetch(&self) -> Vec<PublicKey>; 17 + fn fetch(&self) -> Result<Vec<PublicKey>>; 17 18 } 18 19 19 20 #[derive(Debug, Deserialize)] ··· 32 33 } 33 34 34 35 impl Fetch for Source { 35 - fn fetch(&self) -> Vec<PublicKey> { 36 + fn fetch(&self) -> Result<Vec<PublicKey>> { 36 37 match *self { 37 38 Source::Raw(ref raw) => raw.fetch(), 38 39 Source::Hosts(ref raw) => raw.fetch(), ··· 58 59 } 59 60 } 60 61 61 - fn normalize_sourcehut<'a>(s: &'a str) -> std::borrow::Cow<'a, str> { 62 + fn normalize_sourcehut(s: &str) -> std::borrow::Cow<str> { 62 63 if s.starts_with("~") { 63 64 s.into() 64 65 } else { ··· 70 71 pub struct Raw(Box<[PublicKey]>); 71 72 72 73 impl Fetch for Raw { 73 - fn fetch(&self) -> Vec<PublicKey> { 74 - self.0.clone().into() 74 + fn fetch(&self) -> Result<Vec<PublicKey>> { 75 + Ok(self.0.clone().into()) 75 76 } 76 77 } 77 78 ··· 79 80 pub struct Hosts(pub Box<[String]>); 80 81 81 82 impl Fetch for Hosts { 82 - fn fetch(&self) -> Vec<PublicKey> { 83 + fn fetch(&self) -> Result<Vec<PublicKey>> { 83 84 // TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan` 84 85 let result = Command::new("ssh-keyscan").args(&self.0).output().unwrap(); 85 86 86 - std::str::from_utf8(&result.stdout) 87 - .unwrap() 87 + std::str::from_utf8(&result.stdout)? 88 88 .trim() 89 89 .split('\n') 90 90 .map(str::trim) ··· 93 93 // Ignore first column as it contain hostname which is not 94 94 // needed there 95 95 .map(|line| line.split_once(' ').unwrap().1) 96 - .map(|k| PublicKey::from_openssh(&k).unwrap()) 96 + .map(|k| PublicKey::from_openssh(k).map_err(Into::into)) 97 97 .collect() 98 98 } 99 99 } ··· 104 104 } 105 105 106 106 impl Fetch for Http { 107 - fn fetch(&self) -> Vec<PublicKey> { 107 + fn fetch(&self) -> Result<Vec<PublicKey>> { 108 108 ureq::get(&self.url) 109 - .call() 110 - .unwrap() 109 + .call()? 111 110 .body_mut() 112 - .read_to_string() 113 - .unwrap() 111 + .read_to_string()? 114 112 .trim() 115 113 .split('\n') 116 - .map(|s| PublicKey::from_openssh(s).unwrap()) 114 + .map(|s| PublicKey::from_openssh(s).map_err(Into::into)) 117 115 .collect() 118 116 } 119 117 }