@@ 88,6 88,7 @@ dependencies = [
"native-tls",
"tokio",
"tokio-native-tls",
+ "tokio-util",
"url",
]
@@ 133,6 134,18 @@ dependencies = [
]
[[package]]
+name = "futures-core"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
+
+[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 585,6 598,20 @@ dependencies = [
]
[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1,71 1,96 @@
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use std::convert::TryFrom;
-use tokio::io::{copy, split, stdout as tokio_stdout};
+use std::fmt;
+use std::str;
+use tokio::io::AsyncReadExt;
use tokio::net::TcpStream;
use tokio_native_tls::TlsStream as TokioTlsStream;
#[derive(Debug, Eq, PartialEq)]
enum StatusCode {
/// Status for 1X codes
- Input,
+ INPUT,
/// Status for 2X codes
- Success,
+ SUCCESS,
/// Status for 3X codes
- Redirect,
+ REDIRECT,
/// Status for 4X codes
- TemporaryFailure,
+ TEMPORARYFAILURE,
/// Status for 5X codes
- PermanentFailure,
+ PERMANENTFAILURE,
/// Status for 6X codes
- ClientCertificateRequired,
+ CLIENTCERTIFICATEREQUIRED,
}
-impl TryFrom<u16> for StatusCode {
+impl TryFrom<u8> for StatusCode {
type Error = anyhow::Error;
- fn try_from(code: u16) -> Result<StatusCode> {
- if code < 10 || code > 69 {
+ fn try_from(code: u8) -> Result<StatusCode> {
+ if code < 10 || code >= 70 {
return Err(anyhow!("Status code must be between [10,70), was {}", code));
}
let first_digit = code / 10;
- let code = match first_digit {
- 1 => StatusCode::Input,
- 2 => StatusCode::Success,
- 3 => StatusCode::Redirect,
- 4 => StatusCode::TemporaryFailure,
- 5 => StatusCode::PermanentFailure,
- 6 => StatusCode::ClientCertificateRequired,
+ let status = match first_digit {
+ 1 => StatusCode::INPUT,
+ 2 => StatusCode::SUCCESS,
+ 3 => StatusCode::REDIRECT,
+ 4 => StatusCode::TEMPORARYFAILURE,
+ 5 => StatusCode::PERMANENTFAILURE,
+ 6 => StatusCode::CLIENTCERTIFICATEREQUIRED,
_ => panic!(
"Status code was out of bounds [10,70) but passed the bounds check: {}",
code
),
};
- Ok(code)
+ Ok(status)
}
}
#[derive(Debug, Eq, PartialEq)]
struct Status {
- status: StatusCode,
- code: u16,
+ status_type: StatusCode,
+ code: u8,
}
-impl TryFrom<u16> for Status {
- type Error = anyhow::Error;
+impl Status {
+ /// Convert utf-8 status code bytes into an integer
+ fn bytes_to_code(bytes: &[u8]) -> Result<u8> {
+ let code = str::from_utf8(bytes).context("Error parsing bytes as UTF-8")?;
+ let code = u8::from_str_radix(code, 10).context(format!(
+ "Error converting string \"{}\" to u8 status code",
+ code
+ ))?;
+ if code < 10 || code >= 70 {
+ Err(anyhow!("Status code must be between [10,70), was {}", code))
+ } else {
+ Ok(code)
+ }
+ }
+
+ /// Parse status code from a two byte slice.
+ /// If code is longer than two bytes behavior is undefined
+ /// TODO: Make this more resilient
+ fn try_from(code: &[u8]) -> Result<Status> {
+ let code = Self::bytes_to_code(code).context("Error converting bytes to a status code")?;
- fn try_from(code: u16) -> Result<Status> {
let status = match StatusCode::try_from(code) {
Ok(s) => s,
Err(err) => return Err(err),
};
Ok(Status {
- status: status,
+ status_type: status,
code: code,
})
}
}
+impl fmt::Display for Status {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?} {}", self.status_type, self.code)
+ }
+}
+
type Meta = String;
/// Gemini response headers look like this:
@@ 79,11 104,92 @@ type Meta = String;
/// <META> is a UTF-8 encoded string of maximum length 1024 bytes, whose meaning is <STATUS> dependent.
///
/// <STATUS> and <META> are separated by a single space character.
-struct ResponseHeader {
+///
+/// In bytes:
+/// - 2 bytes: UTF-8 encoded two-digit number
+/// - 1 byte: Space, 0x20
+/// - <= 2014 bytes: METADATA, ending in \r\n
+pub struct ResponseHeader {
status: Status,
meta: Meta,
}
+impl fmt::Display for ResponseHeader {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}: {}", self.status, self.meta)
+ }
+}
+
+pub struct Response {
+ header: ResponseHeader,
+ body: Option<Vec<u8>>,
+}
+
+impl Response {
+ /// Listen on the TokioTlsStream. Parse and print the response.
+ pub async fn handle(mut stream: TokioTlsStream<TcpStream>) -> Result<()> {
+ let mut data = Vec::new();
+ stream
+ .read_to_end(&mut data)
+ .await
+ .context("Error reading from stream")?;
+
+ let r = Response::try_from(data).await?;
+ println!("{}", r);
+ Ok(())
+ }
+
+ /// Try and parse a Response from binary data
+ pub async fn try_from(data: Vec<u8>) -> Result<Response> {
+ // Find index of "\r\n", the end of the meta string
+ let meta_end = data
+ .windows(2)
+ .position(|w| ('\r', '\n') == (w[0] as char, w[1] as char))
+ .expect("No CRLF found in response");
+
+ if meta_end > 1024 + 2 + 1 {
+ return Err(anyhow!(
+ "Metadata was too long. Must be shorter than 1-24 bytes"
+ ));
+ };
+
+ let header = ResponseHeader {
+ status: Status::try_from(&data[0..2]).context("Error parsing status")?,
+ meta: String::from_utf8(data[3..meta_end].to_vec()).context("Error parsing meta")?,
+ };
+
+ let body: Option<Vec<u8>> = match header.status.status_type {
+ StatusCode::SUCCESS => {
+ let body_begin = meta_end + 1;
+ Some(data[body_begin..].to_vec())
+ }
+ _ => None,
+ };
+
+ Ok(Response {
+ header: header,
+ body: body,
+ })
+ }
+}
+
+impl fmt::Display for Response {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let body_string = match &self.body {
+ Some(v) => {
+ if let Ok(s) = String::from_utf8(v.to_vec()) {
+ s
+ } else {
+ String::new()
+ }
+ }
+
+ None => String::new(),
+ };
+ write!(f, "{}\n{}", self.header, body_string)
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ 91,7 197,7 @@ mod tests {
#[test]
fn status_code_parsing() {
let s = StatusCode::try_from(16).unwrap();
- let correct_s = StatusCode::Input;
+ let correct_s = StatusCode::INPUT;
assert_eq!(s, correct_s);
let low_status: Result<StatusCode> = StatusCode::try_from(1);
@@ 107,14 213,3 @@ mod tests {
);
}
}
-
-pub async fn handle_response(stream: TokioTlsStream<TcpStream>) -> Result<()> {
- let (mut reader, _writer) = split(stream);
-
- let mut stdout = tokio_stdout();
-
- println!("Response from server: ");
-
- copy(&mut reader, &mut stdout).await?;
- Ok(())
-}