#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
#![warn(missing_docs)]
pub use reqwest;
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use reqwest::{
StatusCode,
Url,
};
use sequoia_openpgp::{
self as openpgp,
cert::{Cert, CertParser},
KeyHandle,
packet::UserID,
parse::Parse,
serialize::Serialize,
};
#[macro_use] mod macros;
pub mod dane;
mod email;
pub mod updates;
pub mod wkd;
const KEYSERVER_ENCODE_SET: &AsciiSet =
&CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`')
.add(b'?').add(b'{').add(b'}')
.add(b'-').add(b'+').add(b'/');
#[derive(Clone)]
pub struct KeyServer {
client: reqwest::Client,
url: Url,
request_url: Url,
}
assert_send_and_sync!(KeyServer);
impl Default for KeyServer {
fn default() -> Self {
Self::new("hkps://keys.openpgp.org/").unwrap()
}
}
impl KeyServer {
pub fn new(url: &str) -> Result<Self> {
Self::with_client(url, reqwest::Client::new())
}
pub fn with_client(url: &str, client: reqwest::Client) -> Result<Self> {
let url = reqwest::Url::parse(url)?;
let s = url.scheme();
match s {
"hkp" => (),
"hkps" => (),
_ => return Err(Error::MalformedUrl.into()),
}
let request_url =
format!("{}://{}:{}",
match s {"hkp" => "http", "hkps" => "https",
_ => unreachable!()},
url.host().ok_or(Error::MalformedUrl)?,
match s {
"hkp" => url.port().or(Some(11371)),
"hkps" => url.port().or(Some(443)),
_ => unreachable!(),
}.unwrap()).parse()?;
Ok(KeyServer { client, url, request_url })
}
pub fn url(&self) -> &reqwest::Url {
&self.url
}
pub async fn get<H: Into<KeyHandle>>(&self, handle: H)
-> Result<Vec<Result<Cert>>>
{
let handle = handle.into();
let url = self.request_url.join(
&format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?;
let res = self.client.get(url).send().await?;
match res.status() {
StatusCode::OK => {
let body = res.bytes().await?;
let certs = CertParser::from_bytes(&body)?.collect();
Ok(certs)
}
StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
pub async fn search<U: Into<UserID>>(&self, userid: U)
-> Result<Vec<Result<Cert>>>
{
let userid = userid.into();
let email = userid.email().and_then(|addr| addr.ok_or_else(||
openpgp::Error::InvalidArgument(
"UserID does not contain an email address".into()).into()))?;
let url = self.request_url.join(
&format!("pks/lookup?op=get&options=mr&search={}", email))?;
let res = self.client.get(url).send().await?;
match res.status() {
StatusCode::OK => {
Ok(CertParser::from_bytes(&res.bytes().await?)?.collect())
},
StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
pub async fn send(&self, key: &Cert) -> Result<()> {
use sequoia_openpgp::armor::{Writer, Kind};
let url = self.request_url.join("pks/add")?;
let mut w = Writer::new(Vec::new(), Kind::PublicKey)?;
key.serialize(&mut w)?;
let armored_blob = w.finalize()?;
let mut post_data = b"keytext=".to_vec();
post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET)
.collect::<String>().as_bytes());
let length = post_data.len();
let res = self.client.post(url)
.header("content-type", "application/x-www-form-urlencoded")
.header("content-length", length.to_string())
.body(post_data).send().await?;
match res.status() {
StatusCode::OK => Ok(()),
StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
}
pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Cert not found")]
NotFound,
#[error("Malformed URL; expected hkp: or hkps:")]
MalformedUrl,
#[error("Malformed response from server")]
MalformedResponse,
#[error("Protocol violation")]
ProtocolViolation,
#[error("server returned status {0}")]
HttpStatus(hyper::StatusCode),
#[error(transparent)]
UrlError(#[from] url::ParseError),
#[error(transparent)]
HttpError(#[from] http::Error),
#[error(transparent)]
HyperError(#[from] hyper::Error),
#[error("Malformed email address {0}")]
MalformedEmail(String),
#[error("Email address {0} not found in Cert's userids")]
EmailNotInUserids(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls() {
assert!(KeyServer::new("keys.openpgp.org").is_err());
assert!(KeyServer::new("hkp://keys.openpgp.org").is_ok());
assert!(KeyServer::new("hkps://keys.openpgp.org").is_ok());
}
}