quartz_cli/action/
send.rs1use 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 #[arg(long = "var", short = 'v', value_name = "KEY=VALUE")]
21 variables: Vec<String>,
22
23 #[command(flatten)]
24 patch: EndpointPatch,
25
26 #[arg(long)]
28 no_follow: bool,
29
30 #[arg(long = "cookie", short = 'b', value_name = "DATA|FILENAME")]
32 cookies: Vec<String>,
33
34 #[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 .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 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}