use crate::network::Network;
use crate::utils::{BufReadExt, ReadExt};
use crate::{response, Ui};
use bstr::ByteVec;
use std::io::{BufReader, Read, Result as ioResult};
use url::Url;
pub fn run<N: Network>(net: &mut N, ui: &mut impl Ui, mut url: Url)
where
<N as Network>::Error: std::fmt::Debug,
{
while let Some(new_url) = run_url(net, ui, url) {
url = new_url;
}
}
pub(crate) fn run_url<N: Network>(net: &mut N, ui: &mut impl Ui, url: Url) -> Option<Url>
where
<N as Network>::Error: std::fmt::Debug,
{
let mut stream = net.connect(&url).expect("Failed to connect");
send_request(&mut stream, &url).expect("request failed to send");
let head = read_response_header(ui, &mut stream).expect("failed to read header");
handle_response_header(ui, head, url, stream)
}
fn handle_response_header(
ui: &mut impl Ui,
head: response::Header,
url: Url,
stream: impl Read,
) -> Option<Url> {
match head {
response::Header::Input(s) => handle::input(s, ui, url),
response::Header::Success(s) => {
handle::success(s, stream, ui);
None
}
response::Header::Redirect(s) => Some(handle::redirect(s, ui)),
response::Header::FailTemp(s) => {
handle::fail_temp(s, ui);
None
}
response::Header::FailPerm(s) => {
handle::fail_perm(s, ui);
None
}
response::Header::CertRequired(s) => {
handle::cert_required(s, ui);
None
}
}
}
pub mod handle {
use crate::{
decode_media,
response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success},
utils::ReadExt,
Ui,
};
use std::io::Read;
use url::Url;
pub fn cert_required(s: CertRequired, ui: &mut impl Ui) {
let CertRequired { message, typ } = s;
ui.warn(format_args!(
"Certificate required: {typ:?}! Details:\n{message}"
));
}
pub fn fail_perm(s: FailPerm, ui: &mut impl Ui) {
let FailPerm { message, typ } = s;
ui.warn(format_args!(
"Permanent failure: {typ:?}! Details:\n{message}"
));
}
pub fn fail_temp(s: FailTemp, ui: &mut impl Ui) {
let FailTemp { message, typ } = s;
ui.warn(format_args!(
"Temporary failure: {typ:?}! Please try again. Details:\n{message}"
));
}
pub fn redirect(s: Redirect, ui: &mut impl Ui) -> Url {
let Redirect { url, temporary } = s;
if let Some(false) = temporary {
ui.warn(format_args!(
"Server has permanently moved. Redirecting to: {url}\nPlease update your records!"
));
} else {
ui.warn(format_args!(
"Server has temporarily moved. Redirecting to: {url}"
));
}
url
}
pub fn success(s: Success, mut stream: impl Read, ui: &mut impl Ui) {
let Success { mime } = s;
let body = stream.read_string().expect("failed to read body");
let media = decode_media(mime, &body);
ui.show(media);
}
pub fn input(s: Input, ui: &mut impl Ui, mut url: Url) -> Option<Url> {
let Input { prompt, sensitive } = s;
let input = if let Some(true) = sensitive {
ui.read_secret(&prompt)
} else {
ui.read(&prompt)
}
.expect("failed to read input from ui");
let input = urlencoding::encode(&input);
url.set_query(Some(&input));
Some(url)
}
}
pub fn read_response_header(ui: &mut impl Ui, mut stream: impl Read) -> ioResult<response::Header> {
let status = stream.read_n::<2>()?;
let c = stream.read_byte()?;
if c != b' ' {
ui.warn(format_args!(
"illegal separator byte '0x{c:02X}' was not a space"
));
}
let mut meta = Vec::new();
BufReader::new(stream).read_until(b"\r\n", &mut meta)?;
let status = status.map(|s| s.checked_sub(b'0').expect("bad status fragment"));
for _ in 0..2 {
meta.pop();
}
let meta = response::Meta(meta.into_string().expect("Meta was not valid UTF8"));
let status = response::raw::Header { meta, status }
.try_into()
.expect("failed to parse header");
Ok(status)
}
pub fn send_request(stream: &mut impl std::io::Write, url: &Url) -> ioResult<usize> {
let mut sent = 0;
sent += stream.write(url.as_str().as_bytes())?;
sent += stream.write("\r\n".as_bytes())?;
Ok(sent)
}
#[cfg(test)]
mod tests {
use rstest::*;
use url::Url;
use super::{read_response_header, send_request};
use crate::response::{header, Header};
#[rstest]
fn test_handle_redirect(
#[values("http://url.com")] expect_url: Url,
#[values(None, Some(true), Some(false))] temporary: Option<bool>,
) {
let head = Header::Redirect(header::Redirect {
url: expect_url.clone(),
temporary,
});
let mut app = crate::cli::Cli {};
let url = {
let Header::Redirect(s) = head
else {
panic!("expected redirect")
};
crate::core::handle::redirect(s, &mut app)
};
assert_eq!(url, expect_url);
}
#[rstest]
#[case('0', Some(false))]
#[case('1', Some(true))]
#[case('3', None)]
fn test_response_input(#[case] sensitive_code: char, #[case] sensitive: Option<bool>) {
let exp_head = Header::Input(header::Input {
prompt: "prompt text".to_string(),
sensitive,
});
let resp_text = format!("1{sensitive_code} prompt text\r\n");
let resp_head: &[u8] = resp_text.as_bytes();
let head = read_response_header(&mut crate::NopUi, &mut &*resp_head)
.expect("failed to read response");
assert_eq!(exp_head, head);
}
#[test]
fn test_request() {
let url: Url = "gemini://some/url".try_into().unwrap();
let req = b"gemini://some/url\r\n";
let mut net_stream = [0; 1024];
send_request(&mut net_stream.as_mut_slice(), &url).expect("error sending request");
assert_eq!(req, &net_stream[..req.len()]);
}
}