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}