use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::url::Url;
const IO_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_RESPONSE_BYTES: u64 = 64 * 1024 * 1024;
pub fn fetch(url: &Url) -> Result<Vec<u8>> {
let selector = selector_from_path(&url.path)?;
let addr = format!("{}:{}", url.host, url.port);
let tcp = TcpStream::connect(&addr)?;
tcp.set_read_timeout(Some(IO_TIMEOUT))?;
tcp.set_write_timeout(Some(IO_TIMEOUT))?;
let mut request = Vec::with_capacity(selector.len() + 2);
request.extend_from_slice(selector.as_bytes());
request.extend_from_slice(b"\r\n");
if url.is_tls() {
let mut tls = crate::tls::connect_over(tcp, &url.host)?;
tls.write_all(&request)?;
tls.flush()?;
read_capped(&mut tls)
} else {
let mut sock = tcp;
sock.write_all(&request)?;
sock.flush()?;
read_capped(&mut sock)
}
}
fn read_capped<R: Read>(reader: &mut R) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let n = reader.take(MAX_RESPONSE_BYTES + 1).read_to_end(&mut buf)?;
if n as u64 > MAX_RESPONSE_BYTES {
return Err(Error::BadResponse(format!(
"gopher: response exceeds {MAX_RESPONSE_BYTES} bytes"
)));
}
Ok(buf)
}
fn selector_from_path(path: &str) -> Result<&str> {
let without_slash = path.strip_prefix('/').unwrap_or(path);
let mut chars = without_slash.chars();
let selector = match chars.next() {
Some(_) => chars.as_str(),
None => "",
};
if selector.bytes().any(|b| b.is_ascii_control()) {
return Err(Error::InvalidUrl(format!(
"gopher: control byte in selector of path '{path}'"
)));
}
Ok(selector)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selector_root_slash() {
assert_eq!(selector_from_path("/").unwrap(), "");
}
#[test]
fn selector_empty() {
assert_eq!(selector_from_path("").unwrap(), "");
}
#[test]
fn selector_just_item_type() {
assert_eq!(selector_from_path("/1").unwrap(), "");
}
#[test]
fn selector_text_file() {
assert_eq!(selector_from_path("/0foo").unwrap(), "foo");
}
#[test]
fn selector_directory_with_subpath() {
assert_eq!(selector_from_path("/1docs/index").unwrap(), "docs/index");
}
#[test]
fn selector_rejects_crlf_injection() {
assert!(selector_from_path("/0foo\r\nbar").is_err());
assert!(selector_from_path("/0foo\nbar").is_err());
assert!(selector_from_path("/0foo\rbar").is_err());
}
#[test]
fn selector_rejects_nul_and_control_bytes() {
assert!(selector_from_path("/0foo\0bar").is_err());
assert!(selector_from_path("/0foo\x07bar").is_err());
}
#[test]
fn read_capped_accepts_response_at_limit() {
let data = vec![b'x'; 1024];
let mut cur = std::io::Cursor::new(data.clone());
assert_eq!(read_capped(&mut cur).unwrap(), data);
}
#[test]
fn read_capped_rejects_oversized_response() {
let oversized = MAX_RESPONSE_BYTES as usize + 1;
let mut cur = std::io::Cursor::new(vec![0u8; oversized]);
let err = read_capped(&mut cur).unwrap_err();
assert!(matches!(err, Error::BadResponse(_)));
}
}