use std::convert::TryFrom;
#[derive(Debug)]
pub enum RequestError {
IoError(std::io::Error),
DnsError,
TlsError(rustls::Error),
UnknownScheme(String),
ResponseParseError(super::protocol::ResponseParseError),
}
impl std::fmt::Display for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RequestError::IoError(e) => {
write!(f, "IO error: {}", e)
},
RequestError::DnsError => {
write!(f, "DNS Error")
},
RequestError::TlsError(e) => {
write!(f, "TLS Error: {}", e)
},
RequestError::UnknownScheme(s) => {
write!(f, "Unknown scheme {}", s)
},
RequestError::ResponseParseError(e) => {
write!(f, "Response parse error: {}", e)
},
}
}
}
impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RequestError::IoError(e) => Some(e),
RequestError::DnsError => None,
RequestError::TlsError(e) => Some(e),
RequestError::UnknownScheme(_) => None,
RequestError::ResponseParseError(e) => Some(e),
}
}
}
struct DummyVerifier {}
impl DummyVerifier {
pub fn new() -> Self {
Self {}
}
}
impl rustls::client::ServerCertVerifier for DummyVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
Ok(rustls::client::ServerCertVerified::assertion())
}
}
fn open_tcp_stream(url: &crate::url::Url, default_port: u16) -> Result<std::net::TcpStream, RequestError> {
let tcp_stream = match std::net::TcpStream::connect(
url.authority.to_string() + ":" + &url.authority.port.unwrap_or(default_port).to_string(),
) {
Err(e) => return Err(RequestError::IoError(e)),
Ok(s) => s,
};
tcp_stream.set_read_timeout(Some(std::time::Duration::from_secs(15))).unwrap();
Ok(tcp_stream)
}
fn use_stream_do_request(req: &str, stream: &mut dyn std::io::Write) -> Result<(), RequestError> {
match stream.write(req.as_bytes()) {
Err(e) => Err(RequestError::IoError(e)),
Ok(_) => Ok(()),
}
}
fn use_stream_get_resp(stream: &mut dyn std::io::Read) -> Result<crate::protocol::Response, RequestError> {
let mut buffer: Vec<u8> = Vec::new();
match stream.read_to_end(&mut buffer) {
Err(e) => return Err(RequestError::IoError(e)),
Ok(_) => (),
}
parse_merc_gemini_resp(&buffer)
}
fn parse_merc_gemini_resp(resp: &[u8]) -> Result<crate::protocol::Response, RequestError> {
match crate::protocol::Response::try_from(resp) {
Ok(r) => Ok(r),
Err(e) => Err(RequestError::ResponseParseError(e)),
}
}
fn make_gemini_request(
url: &crate::url::Url,
) -> Result<crate::protocol::Response, RequestError> {
use rustls::client::{ClientConfig, ClientConnection};
use std::convert::TryInto;
use std::sync::Arc;
let request = crate::protocol::Request::from(url);
let authority = url.authority.to_string();
let dnsname = match authority.as_str().try_into() {
Ok(s) => s,
Err(_) => return Err(RequestError::DnsError),
};
let cfg = ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(DummyVerifier::new()))
.with_no_client_auth();
let client = ClientConnection::new(Arc::new(cfg), dnsname).unwrap();
let tcp_stream = open_tcp_stream(url, 1965)?;
let mut tls_stream = rustls::StreamOwned::new(client, tcp_stream);
use_stream_do_request(request.raw_string.as_str(), &mut tls_stream)?;
use_stream_get_resp(&mut tls_stream)
}
fn make_mercury_request(
url: &crate::url::Url,
) -> Result<crate::protocol::Response, RequestError> {
let request = crate::protocol::Request::from(url);
let mut stream = open_tcp_stream(url, 1963)?;
use_stream_do_request(request.raw_string.as_str(), &mut stream)?;
use_stream_get_resp(&mut stream)
}
pub fn make_request(url: &crate::url::Url) -> Result<crate::protocol::Response, RequestError> {
match url
.scheme
.as_ref()
.unwrap_or(&"gemini".to_string())
.as_str()
{
"gemini" => make_gemini_request(url),
"mercury" => make_mercury_request(url),
s => Err(RequestError::UnknownScheme(String::from(s))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn make_request_invalid_scheme_error() {
use crate::url::Url;
let raw_url = Url::try_from("https://example.com").unwrap();
let response = make_request(&raw_url).unwrap_err();
match response {
RequestError::UnknownScheme(s) => {
assert_eq!(s, "https");
}
e => {
panic!("Error returned was not an UnknownScheme but instead {:?}", e);
}
}
}
}