~athorp96/Coil

609955b17f0de07c6310be92e1b0db1e7e1131f7 — Andrew Thorp 2 years ago 3b98dab
Add response parsing
5 files changed, 165 insertions(+), 40 deletions(-)

M Cargo.lock
M Cargo.toml
M README.md
M src/main.rs
M src/response.rs
M Cargo.lock => Cargo.lock +27 -0
@@ 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"

M Cargo.toml => Cargo.toml +1 -0
@@ 12,4 12,5 @@ clap = "3.0.0-beta.2"
native-tls = "0.2"
tokio = { version = "1.9.0", features = ["rt-multi-thread", "macros", "net", "io-std", "io-util"] }
tokio-native-tls = "0.3"
tokio-util = { version = "0.6.7", features = ["codec"] }
url = "2.2.2"

M README.md => README.md +1 -1
@@ 26,7 26,7 @@ cargo build --release

## Features
- [X] Send requests to any puclic gemini server.
- [] Parse response intelligently.
- [X] Parse response intelligently.
- [] Add input to requests.
- [] Add certificate files to requests.
- [] Friendly render `text/gemini` mime type.

M src/main.rs => src/main.rs +3 -1
@@ 10,6 10,8 @@ use tokio_native_tls::TlsStream as TokioTlsStream;
mod request;
mod response;

use response::Response;

/// Coil is a Curl-like client for the Gemini web protocol.
#[derive(Clap)]
#[clap(version = "0.1", author = "Andrew Thorp. <andrew.thorp.dev@gmail.com>")]


@@ 65,7 67,7 @@ async fn main() -> Result<()> {

    stream.write_all(&req.as_bytes()).await?;

    response::handle_response(stream)
    Response::handle(stream)
        .await
        .context("Error handling response")?;


M src/response.rs => src/response.rs +133 -38
@@ 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(())
}