1#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
38#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
39#![warn(missing_docs)]
40
41pub use reqwest;
43
44use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
45
46use reqwest::{
47 StatusCode,
48 Url,
49};
50
51use sequoia_openpgp::{
52 self as openpgp,
53 cert::{Cert, CertParser},
54 KeyHandle,
55 packet::UserID,
56 parse::Parse,
57 serialize::Serialize,
58};
59
60#[macro_use] mod macros;
61pub mod dane;
62mod email;
63pub mod updates;
64pub mod wkd;
65
66const KEYSERVER_ENCODE_SET: &AsciiSet =
68 &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`')
70 .add(b'?').add(b'{').add(b'}')
71 .add(b'-').add(b'+').add(b'/');
74
75#[derive(Clone)]
77pub struct KeyServer {
78 client: reqwest::Client,
79 url: Url,
81 request_url: Url,
83}
84
85assert_send_and_sync!(KeyServer);
86
87impl Default for KeyServer {
88 fn default() -> Self {
89 Self::new("hkps://keys.openpgp.org/").unwrap()
90 }
91}
92
93impl KeyServer {
94 pub fn new(url: &str) -> Result<Self> {
96 Self::with_client(url, reqwest::Client::new())
97 }
98
99 pub fn with_client(url: &str, client: reqwest::Client) -> Result<Self> {
101 let url = reqwest::Url::parse(url)?;
102
103 let s = url.scheme();
104 match s {
105 "hkp" => (),
106 "hkps" => (),
107 _ => return Err(Error::MalformedUrl.into()),
108 }
109
110 let request_url =
111 format!("{}://{}:{}",
112 match s {"hkp" => "http", "hkps" => "https",
113 _ => unreachable!()},
114 url.host().ok_or(Error::MalformedUrl)?,
115 match s {
116 "hkp" => url.port().or(Some(11371)),
117 "hkps" => url.port().or(Some(443)),
118 _ => unreachable!(),
119 }.unwrap()).parse()?;
120
121 Ok(KeyServer { client, url, request_url })
122 }
123
124 pub fn url(&self) -> &reqwest::Url {
126 &self.url
127 }
128
129 pub async fn get<H: Into<KeyHandle>>(&self, handle: H)
136 -> Result<Vec<Result<Cert>>>
137 {
138 let handle = handle.into();
139 let url = self.request_url.join(
140 &format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?;
141
142 let res = self.client.get(url).send().await?;
143 match res.status() {
144 StatusCode::OK => {
145 let body = res.bytes().await?;
146 let certs = CertParser::from_bytes(&body)?.collect();
147 Ok(certs)
148 }
149 StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
150 n => Err(Error::HttpStatus(n).into()),
151 }
152 }
153
154 pub async fn search<U: Into<UserID>>(&self, userid: U)
167 -> Result<Vec<Result<Cert>>>
168 {
169 let userid = userid.into();
170 let email = userid.email().and_then(|addr| addr.ok_or_else(||
171 openpgp::Error::InvalidArgument(
172 "UserID does not contain an email address".into()).into()))?;
173 let url = self.request_url.join(
174 &format!("pks/lookup?op=get&options=mr&search={}", email))?;
175
176 let res = self.client.get(url).send().await?;
177 match res.status() {
178 StatusCode::OK => {
179 Ok(CertParser::from_bytes(&res.bytes().await?)?.collect())
180 },
181 StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
182 n => Err(Error::HttpStatus(n).into()),
183 }
184 }
185
186 pub async fn send(&self, key: &Cert) -> Result<()> {
188 use sequoia_openpgp::armor::{Writer, Kind};
189
190 let url = self.request_url.join("pks/add")?;
191 let mut w = Writer::new(Vec::new(), Kind::PublicKey)?;
192 key.serialize(&mut w)?;
193
194 let armored_blob = w.finalize()?;
195
196 let mut post_data = b"keytext=".to_vec();
198 post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET)
199 .collect::<String>().as_bytes());
200 let length = post_data.len();
201
202 let res = self.client.post(url)
203 .header("content-type", "application/x-www-form-urlencoded")
204 .header("content-length", length.to_string())
205 .body(post_data).send().await?;
206
207 match res.status() {
208 StatusCode::OK => Ok(()),
209 StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()),
210 n => Err(Error::HttpStatus(n).into()),
211 }
212 }
213}
214
215pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
217
218#[derive(thiserror::Error, Debug)]
219#[non_exhaustive]
221pub enum Error {
222 #[error("Cert not found")]
224 NotFound,
225 #[error("Malformed URL; expected hkp: or hkps:")]
227 MalformedUrl,
228 #[error("Malformed response from server")]
230 MalformedResponse,
231 #[error("Protocol violation")]
233 ProtocolViolation,
234 #[error("server returned status {0}")]
236 HttpStatus(hyper::StatusCode),
237 #[error(transparent)]
239 UrlError(#[from] url::ParseError),
240 #[error(transparent)]
242 HttpError(#[from] http::Error),
243 #[error(transparent)]
245 HyperError(#[from] hyper::Error),
246
247 #[error("Malformed email address {0}")]
250 MalformedEmail(String),
251
252 #[error("Email address {0} not found in Cert's userids")]
254 EmailNotInUserids(String),
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn urls() {
263 assert!(KeyServer::new("keys.openpgp.org").is_err());
264 assert!(KeyServer::new("hkp://keys.openpgp.org").is_ok());
265 assert!(KeyServer::new("hkps://keys.openpgp.org").is_ok());
266 }
267}