gophers/
lib.rs

1/* This file is part of gophers (https://github.com/parazyd/gophers)
2 *
3 * Copyright (C) 2023 parazyd <parazyd@dyne.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    io,
21    io::{Read, Write},
22    net::TcpStream,
23};
24
25use native_tls::{TlsConnector, TlsStream};
26use url::Url;
27
28/// Exported library error types
29#[derive(thiserror::Error, Debug)]
30pub enum GopherError {
31    #[error("Invalid host")]
32    InvalidHost,
33
34    #[error("Unsupported protocol")]
35    UnsupportedProtocol,
36
37    #[error(transparent)]
38    HandshakeError(#[from] native_tls::HandshakeError<TcpStream>),
39
40    #[error(transparent)]
41    TlsError(#[from] native_tls::Error),
42
43    #[error(transparent)]
44    IoError(#[from] io::Error),
45
46    #[error(transparent)]
47    UrlParseError(#[from] url::ParseError),
48}
49
50/// The Gopher struct represents an initialized object ready to connect.
51pub struct Gopher {
52    host: String,
53    port: u16,
54    tls: bool,
55}
56
57impl Gopher {
58    /// Create a new `Gopher` object with the given endpoint.
59    ///
60    /// # Example
61    /// ```
62    /// use gophers::Gopher;
63    /// let gopher = Gopher::new("gophers://bitreich.org").unwrap();
64    /// ```
65    pub fn new(endpoint: &str) -> Result<Self, GopherError> {
66        let url = Url::parse(endpoint)?;
67
68        if url.host().is_none() {
69            return Err(GopherError::InvalidHost);
70        }
71
72        let (host, tls) = match url.scheme() {
73            "gopher" => (url.host().unwrap(), false),
74            "gophers" => (url.host().unwrap(), true),
75            _ => return Err(GopherError::UnsupportedProtocol),
76        };
77
78        Ok(Self {
79            host: host.to_string(),
80            port: url.port().unwrap_or(70),
81            tls,
82        })
83    }
84
85    /// Establish a connection with a created Gopher object.
86    /// Depending on `tls`, it will establish either a plain TCP or an
87    /// encrypted TLS connection.
88    ///
89    /// # Example
90    /// ```
91    /// use gophers::Gopher;
92    /// let gopher = Gopher::new("gophers://bitreich.org").unwrap();
93    /// let mut stream = gopher.connect().unwrap();
94    /// ```
95    pub fn connect(&self) -> Result<GopherConnection, GopherError> {
96        let tcp_conn = TcpStream::connect(format!("{}:{}", self.host, self.port))?;
97
98        if !self.tls {
99            return Ok(GopherConnection::Tcp(tcp_conn));
100        }
101
102        let tls_conn = TlsConnector::new()?;
103        let stream = tls_conn.connect(&self.host, tcp_conn)?;
104
105        Ok(GopherConnection::Tls(stream))
106    }
107}
108
109/// Abstraction enum over TCP and TLS connections.
110/// Implements both `Read` and `Write` traits.
111pub enum GopherConnection {
112    Tcp(TcpStream),
113    Tls(TlsStream<TcpStream>),
114}
115
116impl Write for GopherConnection {
117    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
118        match self {
119            Self::Tcp(c) => c.write(buf),
120            Self::Tls(c) => c.write(buf),
121        }
122    }
123
124    fn flush(&mut self) -> io::Result<()> {
125        match self {
126            Self::Tcp(c) => c.flush(),
127            Self::Tls(c) => c.flush(),
128        }
129    }
130}
131
132impl Read for GopherConnection {
133    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
134        match self {
135            Self::Tcp(c) => c.read(buf),
136            Self::Tls(c) => c.read(buf),
137        }
138    }
139}
140
141impl GopherConnection {
142    /// Fetch a resource given a path from an established Gopher connection.
143    ///
144    /// # Example
145    /// ```
146    /// use gophers::Gopher;
147    /// let gopher = Gopher::new("gophers://bitreich.org").unwrap();
148    /// let mut stream = gopher.connect().unwrap();
149    /// let data = stream.fetch("/memecache/index.meme").unwrap();
150    /// assert_eq!(&data[..5], b"meme2");
151    /// ```
152    pub fn fetch(&mut self, path: &str) -> Result<Vec<u8>, io::Error> {
153        let req = format!("{}\r\n", path);
154        self.write_all(req.as_bytes())?;
155        let mut buf = vec![];
156        self.read_to_end(&mut buf)?;
157        Ok(buf)
158    }
159}