gemini_fetch/
lib.rs

1use std::convert::TryFrom;
2use std::net::{SocketAddr, ToSocketAddrs};
3use std::str::FromStr;
4use std::sync::Arc;
5
6// TODO: make this use all specific thiserror errors
7use anyhow::{format_err, Result};
8use rustls::{
9    Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError,
10};
11use thiserror::Error;
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13use tokio::net::TcpStream;
14use tokio_rustls::TlsConnector;
15use url::Url;
16use webpki::DNSNameRef;
17
18const REDIRECT_CAP: usize = 5;
19
20/// Gemini response codes
21///
22/// See https://gemini.circumlunar.space/docs/specification.html
23/// for more information.
24#[derive(Debug, PartialEq)]
25pub enum Status {
26    // 10
27    Input,
28    // 11
29    SensitiveInput,
30    // 20
31    Success,
32    // 30
33    TemporaryRedirect,
34    // 31
35    PermanentRedirect,
36    // 40
37    TemporaryFailure,
38    // 41
39    ServerUnavailable,
40    // 42
41    CgiError,
42    // 43
43    ProxyError,
44    // 44
45    SlowDown,
46    // 50
47    PermanentFailure,
48    // 51
49    NotFound,
50    // 52
51    Gone,
52    // 53
53    ProxyRequestRefused,
54    // 59
55    BadRequest,
56    // 60
57    ClientCertificateRequired,
58    // 61
59    CertificateNotAuthorized,
60    // 62
61    CertificateNotValid,
62}
63
64#[derive(Debug, Error)]
65pub enum ParseStatusError {
66    #[error("invalid status \"{0}\"")]
67    InvalidStatus(String),
68}
69
70impl FromStr for Status {
71    type Err = ParseStatusError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        match s {
75            "10" => Ok(Status::Input),
76            "11" => Ok(Status::SensitiveInput),
77            "20" => Ok(Status::Success),
78            "30" => Ok(Status::TemporaryRedirect),
79            "31" => Ok(Status::PermanentRedirect),
80            "40" => Ok(Status::TemporaryFailure),
81            "41" => Ok(Status::ServerUnavailable),
82            "42" => Ok(Status::CgiError),
83            "43" => Ok(Status::ProxyError),
84            "44" => Ok(Status::SlowDown),
85            "50" => Ok(Status::PermanentFailure),
86            "51" => Ok(Status::NotFound),
87            "52" => Ok(Status::Gone),
88            "53" => Ok(Status::ProxyRequestRefused),
89            "59" => Ok(Status::BadRequest),
90            "60" => Ok(Status::ClientCertificateRequired),
91            "61" => Ok(Status::CertificateNotAuthorized),
92            "62" => Ok(Status::CertificateNotValid),
93            _ => Err(ParseStatusError::InvalidStatus(s.to_string())),
94        }
95    }
96}
97
98/// Gemini page's single header
99///
100/// See https://gemini.circumlunar.space/docs/specification.html
101/// for more information.
102#[derive(Debug, PartialEq)]
103pub struct Header {
104    /// Header's status
105    pub status: Status,
106
107    /// Header's metadata string
108    pub meta: String,
109}
110
111#[derive(Debug, Error)]
112pub enum ParseHeaderError {
113    #[error("missing status")]
114    MissingStatus,
115    #[error("missing meta")]
116    MissingMeta,
117    #[error(transparent)]
118    InvalidStatus(#[from] ParseStatusError),
119}
120
121impl FromStr for Header {
122    type Err = ParseHeaderError;
123
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        let parts: Vec<&str> = s.trim().splitn(2, ' ').collect();
126
127        let status: Status = parts
128            .get(0)
129            .ok_or(ParseHeaderError::MissingStatus)?
130            .parse()?;
131        let meta = parts.get(1).ok_or(ParseHeaderError::MissingMeta)?;
132
133        Ok(Header {
134            status,
135            meta: meta.to_string(),
136        })
137    }
138}
139
140/// Single Gemini page
141///
142/// See https://gemini.circumlunar.space/docs/specification.html
143/// for more information.
144#[derive(Debug)]
145pub struct Page {
146    /// Page's URL
147    pub url: Url,
148
149    /// Page's single header
150    pub header: Header,
151
152    /// Page's optional body
153    pub body: Option<String>,
154}
155
156pub enum ServerTLSValidation {
157    SelfSigned(CertificateFingerprint),
158    Chained,
159}
160
161pub struct CertificateFingerprint {
162    digest: [u8; ring::digest::SHA256_OUTPUT_LEN],
163    not_after: i64,
164}
165
166fn map_sig_to_webpki_err(e: x509_signature::Error) -> webpki::Error {
167    match e {
168        x509_signature::Error::UnsupportedCertVersion => webpki::Error::UnsupportedCertVersion,
169        x509_signature::Error::UnsupportedSignatureAlgorithm => {
170            webpki::Error::UnsupportedSignatureAlgorithm
171        }
172        x509_signature::Error::UnsupportedSignatureAlgorithmForPublicKey => {
173            webpki::Error::UnsupportedSignatureAlgorithmForPublicKey
174        }
175        x509_signature::Error::InvalidSignatureForPublicKey => {
176            webpki::Error::InvalidSignatureForPublicKey
177        }
178        x509_signature::Error::SignatureAlgorithmMismatch => {
179            webpki::Error::SignatureAlgorithmMismatch
180        }
181        x509_signature::Error::BadDER => webpki::Error::BadDER,
182        x509_signature::Error::BadDERTime => webpki::Error::BadDERTime,
183        x509_signature::Error::CertNotValidYet => webpki::Error::CertNotValidYet,
184        x509_signature::Error::CertExpired => webpki::Error::CertExpired,
185        x509_signature::Error::InvalidCertValidity => webpki::Error::InvalidCertValidity,
186        x509_signature::Error::UnknownIssuer => webpki::Error::UnknownIssuer,
187        // TODO: This is a shitty default, but this should be a "lossless" conversion - i.e. we
188        // can't really give back an error of a different type
189        _ => webpki::Error::UnknownIssuer,
190    }
191}
192
193fn unix_now() -> Result<i64, rustls::TLSError> {
194    let now = std::time::SystemTime::now();
195    let unix_now = now
196        .duration_since(std::time::UNIX_EPOCH)
197        .map_err(|_| TLSError::FailedToGetCurrentTime)?
198        .as_secs();
199
200    i64::try_from(unix_now).map_err(|_| TLSError::FailedToGetCurrentTime)
201}
202
203fn verify_selfsigned_certificate(
204    cert: &Certificate,
205    _dns_name: DNSNameRef<'_>,
206    now: i64,
207) -> Result<ServerCertVerified, x509_signature::Error> {
208    let xcert = x509_signature::parse_certificate(cert.as_ref())?;
209    xcert.valid_at_timestamp(now)?;
210    xcert.check_self_issued()?;
211    // TODO: this doesn't check the subject name, but this is a self signed cert,
212    // so this is basically the wild west anyways. do we care?
213    Ok(ServerCertVerified::assertion())
214}
215
216struct ExpectSelfSignedVerifier {
217    webpki: rustls::WebPKIVerifier,
218    fingerprint: CertificateFingerprint,
219}
220
221impl ServerCertVerifier for ExpectSelfSignedVerifier {
222    fn verify_server_cert(
223        &self,
224        roots: &RootCertStore,
225        presented_certs: &[Certificate],
226        dns_name: DNSNameRef<'_>,
227        ocsp_response: &[u8],
228    ) -> Result<ServerCertVerified, TLSError> {
229        // This is a special case for when the client presents a self-signed certificate
230        if presented_certs.len() == 1 {
231            let now = unix_now()?;
232
233            if now > self.fingerprint.not_after {
234                // The fingerprint is valid - hash & compare the presented certificate
235                let dig =
236                    ring::digest::digest(&ring::digest::SHA256, presented_certs[0].0.as_ref());
237                if let Ok(()) = ring::constant_time::verify_slices_are_equal(
238                    dig.as_ref(),
239                    &self.fingerprint.digest,
240                ) {
241                    return Ok(ServerCertVerified::assertion());
242                }
243            } else {
244                return verify_selfsigned_certificate(&presented_certs[0], dns_name, now)
245                    .map_err(map_sig_to_webpki_err)
246                    .map_err(rustls::TLSError::WebPKIError);
247            }
248        }
249
250        let verified =
251            self.webpki
252                .verify_server_cert(roots, presented_certs, dns_name, ocsp_response)?;
253
254        Ok(verified)
255    }
256}
257
258struct PossiblySelfSignedVerifier {
259    webpki: rustls::WebPKIVerifier,
260}
261
262impl ServerCertVerifier for PossiblySelfSignedVerifier {
263    fn verify_server_cert(
264        &self,
265        roots: &RootCertStore,
266        presented_certs: &[Certificate],
267        dns_name: DNSNameRef<'_>,
268        ocsp_response: &[u8],
269    ) -> Result<ServerCertVerified, TLSError> {
270        // This is a special case for when it looks like the client presents a self-signed
271        // certificate
272        if presented_certs.len() == 1 {
273            let verified =
274                verify_selfsigned_certificate(&presented_certs[0], dns_name, unix_now()?)
275                    .map_err(map_sig_to_webpki_err)
276                    .map_err(TLSError::WebPKIError)?;
277
278            return Ok(verified);
279        }
280
281        let verified =
282            self.webpki
283                .verify_server_cert(roots, presented_certs, dns_name, ocsp_response)?;
284
285        Ok(verified)
286    }
287}
288
289async fn build_tls_config<'a>(
290    validation: Option<ServerTLSValidation>,
291) -> Result<Arc<ClientConfig>> {
292    let mut config = ClientConfig::new();
293    config
294        .root_store
295        .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
296    match validation {
297        None => {
298            config
299                .dangerous()
300                .set_certificate_verifier(Arc::new(PossiblySelfSignedVerifier {
301                    webpki: rustls::WebPKIVerifier::new(),
302                }));
303        }
304        Some(ServerTLSValidation::SelfSigned(fingerprint)) => {
305            config
306                .dangerous()
307                .set_certificate_verifier(Arc::new(ExpectSelfSignedVerifier {
308                    fingerprint,
309                    webpki: rustls::WebPKIVerifier::new(),
310                }));
311        }
312        _ => {}
313    }
314
315    Ok(Arc::new(config))
316}
317
318#[derive(Debug, Error)]
319pub enum FetchPageError {
320    #[error("unsupported scheme \"{0}\", only gemini is supported")]
321    UnsupportedScheme(String),
322    #[error("missing host in URL \"{0}\"")]
323    MissingHost(String),
324    #[error("failed to resolve URL \"{0}\"")]
325    FailedToResolve(String),
326    #[error("response is missing its header")]
327    MissingHeader,
328}
329
330impl Page {
331    /// Fetch the given Gemini link.
332    ///
333    /// Does not follow redirects or other status codes.
334    pub async fn fetch(url: &Url, tls_validation: Option<ServerTLSValidation>) -> Result<Page> {
335        let host = url
336            .host_str()
337            .ok_or_else(|| FetchPageError::MissingHost(url.to_string()))?;
338
339        let port = url.port().unwrap_or(1965);
340
341        let addr = format!("{}:{}", host, port)
342            .to_socket_addrs()?
343            .next()
344            .ok_or_else(|| FetchPageError::FailedToResolve(url.to_string()))?;
345
346        Self::fetch_from(url, addr, tls_validation).await
347    }
348
349    /// Fetch the given Gemini link from the specified socket address (e.g. a proxy).
350    ///
351    /// Does not follow redirects or other status codes.
352    pub async fn fetch_from(
353        url: &Url,
354        addr: SocketAddr,
355        tls_validation: Option<ServerTLSValidation>,
356    ) -> Result<Page> {
357        if url.scheme() != "gemini" {
358            return Err(FetchPageError::UnsupportedScheme(url.to_string()).into());
359        }
360
361        let host = url
362            .host_str()
363            .ok_or_else(|| FetchPageError::MissingHost(url.to_string()))?;
364
365        let dns_name = DNSNameRef::try_from_ascii_str(&host)?;
366        let socket = TcpStream::connect(&addr).await?;
367        let config = TlsConnector::from(build_tls_config(tls_validation).await?);
368
369        let mut socket = config.connect(dns_name, socket).await?;
370
371        socket.write_all(format!("{}\r\n", url).as_bytes()).await?;
372
373        let mut data = Vec::new();
374        socket.read_to_end(&mut data).await?;
375
376        let mut response = String::from_utf8(data)?.replace("\r\n", "\n");
377
378        let (header, body) = if let Some(i) = response.find('\n') {
379            let remainder = response.split_off(i + 1);
380            (response.parse::<Header>()?, remainder)
381        } else {
382            return Err(FetchPageError::MissingHeader.into());
383        };
384
385        Ok(Page {
386            url: url.clone(),
387            header,
388            body: if body.is_empty() { None } else { Some(body) },
389        })
390    }
391
392    /// Fetch the given Gemini link while following redirects
393    pub async fn fetch_and_handle_redirects(url: &Url) -> Result<Page> {
394        let mut url = url.clone();
395
396        let mut attempts = 0;
397        while attempts < REDIRECT_CAP {
398            // TODO: verification
399            let page = Page::fetch(&url, None).await?;
400
401            if let Status::TemporaryRedirect | Status::PermanentRedirect = page.header.status {
402                attempts += 1;
403                url = Url::parse(&page.header.meta)?;
404            } else {
405                return Ok(page);
406            }
407        }
408
409        Err(format_err!(
410            "reached maximum redirect cap of {}",
411            REDIRECT_CAP
412        ))
413    }
414}