quartz_cli/action/
send.rs

1use crate::{
2    cookie::CookieJar,
3    endpoint::EndpointPatch,
4    history::{self, History},
5    Ctx, PairMap, QuartzResult,
6};
7use chrono::Utc;
8use hyper::{
9    body::{Bytes, HttpBody},
10    header::{HeaderName, HeaderValue},
11    Body, Client, Uri,
12};
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15use tokio::io::{stdout, AsyncWriteExt as _};
16
17#[derive(clap::Args, Debug)]
18pub struct Args {
19    /// Change a variable when sending the request.
20    #[arg(long = "var", short = 'v', value_name = "KEY=VALUE")]
21    variables: Vec<String>,
22
23    #[command(flatten)]
24    patch: EndpointPatch,
25
26    /// Do not follow redirects
27    #[arg(long)]
28    no_follow: bool,
29
30    /// Pass cookie data to request header
31    #[arg(long = "cookie", short = 'b', value_name = "DATA|FILENAME")]
32    cookies: Vec<String>,
33
34    /// Which file to write all cookies after a completed request
35    #[arg(long, short = 'c', value_name = "FILE")]
36    cookie_jar: Option<PathBuf>,
37}
38
39pub async fn cmd(ctx: &Ctx, mut args: Args) -> QuartzResult {
40    let (handle, mut endpoint) = ctx.require_endpoint();
41    let mut env = ctx.require_env();
42    for var in args.variables {
43        env.variables.set(&var);
44    }
45
46    if !endpoint.headers.contains_key("user-agent") {
47        endpoint
48            .headers
49            .insert("user-agent".to_string(), Ctx::user_agent());
50    }
51
52    let mut cookie_jar = env.cookie_jar(ctx);
53
54    let extras = args.cookies.iter().flat_map(|c| {
55        if c.contains('=') {
56            return vec![c.to_owned()];
57        }
58
59        let path = Path::new(c);
60        if !path.exists() {
61            panic!("no such file: {c}");
62        }
63
64        CookieJar::read(path)
65            .unwrap()
66            .iter()
67            .map(|c| format!("{}={}", c.name(), c.value()))
68            .collect()
69    });
70
71    let cookie_value = cookie_jar
72        .iter()
73        .map(|c| format!("{}={}", c.name(), c.value()))
74        .chain(extras)
75        .collect::<Vec<String>>()
76        .join("; ");
77
78    if !cookie_value.is_empty() {
79        endpoint
80            .headers
81            .insert(String::from("Cookie"), cookie_value);
82    }
83
84    let mut entry = history::Entry::builder();
85    entry
86        .handle(handle.handle())
87        .timestemp(Utc::now().timestamp_micros());
88
89    endpoint.update(&mut args.patch);
90    endpoint.apply_env(&env);
91
92    let body = endpoint.body().cloned();
93
94    let mut res: hyper::Response<Body>;
95
96    loop {
97        let mut req = endpoint
98            // TODO: Find a way around this clone
99            .clone()
100            .into_request()
101            .unwrap_or_else(|_| panic!("malformed request"));
102        for (key, val) in env.headers.iter() {
103            if !endpoint.headers.contains_key(key) {
104                req.headers_mut()
105                    .insert(HeaderName::from_str(key)?, HeaderValue::from_str(val)?);
106            }
107        }
108
109        entry.message(&req);
110        if let Some(ref body) = body {
111            entry.message_raw(body.to_owned());
112        }
113
114        let client = {
115            let https = hyper_tls::HttpsConnector::new();
116            Client::builder().build(https)
117        };
118
119        res = client.request(req).await?;
120
121        entry.message(&res);
122
123        if let Some(cookie_header) = res.headers().get("Set-Cookie") {
124            let url = endpoint.full_url()?;
125
126            cookie_jar.set(url.host().unwrap(), cookie_header.to_str()?);
127        }
128
129        if args.no_follow || !res.status().is_redirection() {
130            break;
131        }
132
133        if let Some(location) = res.headers().get("Location") {
134            let location = location.to_str()?;
135
136            if location.starts_with('/') {
137                let url = endpoint.full_url()?;
138                // This is awful
139                endpoint.url = Uri::builder()
140                    .authority(url.authority().unwrap().as_str())
141                    .scheme(url.scheme().unwrap().as_str())
142                    .path_and_query(location)
143                    .build()?
144                    .to_string();
145            } else if Uri::from_str(location).is_ok() {
146                endpoint.url = location.to_string();
147            }
148        };
149    }
150
151    match args.cookie_jar {
152        Some(path) => cookie_jar.write_at(&path)?,
153        None => cookie_jar.write()?,
154    };
155
156    let mut bytes = Bytes::new();
157
158    while let Some(chunk) = res.data().await {
159        if let Ok(chunk) = chunk {
160            bytes = [bytes, chunk].concat().into();
161        }
162    }
163
164    entry.message_raw(String::from_utf8(bytes.to_vec())?);
165
166    let _ = stdout().write_all(&bytes).await;
167    History::write(ctx, entry.build()?)?;
168
169    Ok(())
170}