basic client operations

This commit is contained in:
Cadey Ratio 2020-07-25 10:56:46 -04:00
parent b6d062ff8e
commit c7d8767bf6
4 changed files with 86 additions and 5 deletions

View File

@ -12,6 +12,7 @@ num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
rustls = "0.18" rustls = "0.18"
webpki = "0.21.0" webpki = "0.21.0"
webpki-roots = "0.20"
log = "0.4" log = "0.4"
url = "2" url = "2"
thiserror = "1" thiserror = "1"

64
src/client.rs Normal file
View File

@ -0,0 +1,64 @@
use crate::{Response, StatusCode};
use rustls::{ClientConfig, ClientSession, Stream, TLSError};
use std::{io::prelude::*, net::TcpStream, sync::Arc};
use url::Url;
fn config() -> ClientConfig {
let mut config = ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
config
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("TLS error: {0:?}")]
TLS(#[from] TLSError),
#[error("URL error: {0:?}")]
URL(#[from] url::ParseError),
#[error("Invalid DNS name: {0:?}")]
InvalidDNSName(#[from] webpki::InvalidDNSNameError),
#[error("IO error: {0:?}")]
IO(#[from] std::io::Error),
#[error("Response parsing error: {0:?}")]
ResponseParse(#[from] crate::ResponseError),
}
pub fn get(u: String) -> Result<crate::Response, Error> {
let mut ur = Url::parse(&u.clone())?;
if ur.port().is_none() {
ur.set_port(Some(1965)).unwrap();
}
let cfg = Arc::new(config());
let host = ur.host_str().unwrap();
let mut sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap()))?;
let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?;
let mut client = ClientSession::new(&cfg, name_ref);
let mut tls = Stream::new(&mut client, &mut sock);
let req = format!("{}\r\n", u);
log::trace!("writing request {:?}", req);
tls.write(req.as_bytes())?;
Ok(Response::parse(&mut tls)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gemini_homepage() -> Result<(), Error> {
let _ = pretty_env_logger::try_init();
let resp = get("gemini://gemini.circumlunar.space/".to_string())?;
assert_eq!(resp.status, StatusCode::Success);
assert_eq!(resp.meta, "text/gemini");
assert_ne!(resp.body.len(), 0);
Ok(())
}
}

View File

@ -1,5 +1,7 @@
mod client;
mod status_code; mod status_code;
mod response; mod response;
pub use status_code::StatusCode; pub use status_code::StatusCode;
pub use response::{Response, Error as ResponseError}; pub use response::{Response, Error as ResponseError};
pub use client::{get, Error as ClientError};

View File

@ -1,13 +1,13 @@
use crate::StatusCode; use crate::StatusCode;
use num::FromPrimitive; use num::FromPrimitive;
use std::io::prelude::*; use std::io::{prelude::*, ErrorKind};
/// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html). /// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html).
#[derive(Default)] #[derive(Default)]
pub struct Response { pub struct Response {
status: StatusCode, pub status: StatusCode,
meta: String, pub meta: String,
body: Vec<u8>, pub body: Vec<u8>,
} }
/// The parser state. /// The parser state.
@ -39,6 +39,9 @@ pub enum Error {
#[error("None found when none should not be found")] #[error("None found when none should not be found")]
NoneFound, NoneFound,
#[error("TLS error: {0:?}")]
TLS(#[from] rustls::TLSError),
} }
impl Response { impl Response {
@ -57,8 +60,19 @@ impl Response {
} }
} }
log::trace!("buf: {:?}: {:?}", buf, buf[0] as char); log::trace!("buf: {:?}: {:?}", buf, buf[0] as char);
log::trace!("n: {}", n);
}
Err(why) => {
if why.kind() == ErrorKind::ConnectionAborted {
if let State::ReadBody { data } = state {
result.body = data;
return Ok(result);
}
}
return Err(Error::IO(why));
} }
Err(why) => return Err(Error::IO(why)),
} }
// log::trace!("state: {:?}", state); // log::trace!("state: {:?}", state);