|
|
|
@ -1,5 +1,7 @@
|
|
|
|
|
use clap::{Parser, ValueEnum};
|
|
|
|
|
use futures::stream::StreamExt;
|
|
|
|
|
use json::JsonValue;
|
|
|
|
|
use reqwest::header::HeaderMap;
|
|
|
|
|
use reqwest::{
|
|
|
|
|
header::{HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE, LOCATION},
|
|
|
|
|
redirect, Body, RequestBuilder, Response, StatusCode, Url, Version,
|
|
|
|
@ -44,6 +46,10 @@ struct Params {
|
|
|
|
|
#[arg(short = 'I', long, conflicts_with = "include_headers")]
|
|
|
|
|
headers: bool,
|
|
|
|
|
|
|
|
|
|
/// Never pretty-print supported content types
|
|
|
|
|
#[arg(short = 'r', long)]
|
|
|
|
|
raw: bool,
|
|
|
|
|
|
|
|
|
|
/// Shortcut for setting the `Content-Type` header
|
|
|
|
|
#[arg(short = 't', long)]
|
|
|
|
|
content_type: Option<String>,
|
|
|
|
@ -140,7 +146,8 @@ async fn do_request(params: Params) -> Option<String> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !params.headers {
|
|
|
|
|
let len = print_body(response).await;
|
|
|
|
|
let pretty = !params.raw && isatty(1);
|
|
|
|
|
let len = print_body(response, pretty).await;
|
|
|
|
|
if len == 0 && params.verbose {
|
|
|
|
|
eprintln!("Response body is empty");
|
|
|
|
|
}
|
|
|
|
@ -150,20 +157,119 @@ async fn do_request(params: Params) -> Option<String> {
|
|
|
|
|
redirect_url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn print_body(response: Response) -> usize {
|
|
|
|
|
async fn print_body(response: Response, pretty: bool) -> usize {
|
|
|
|
|
if pretty && content_type_is_json(response.headers()) {
|
|
|
|
|
return print_body_json(response).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut stdout = stdout();
|
|
|
|
|
let stream = response.bytes_stream();
|
|
|
|
|
tokio::pin!(stream);
|
|
|
|
|
|
|
|
|
|
let mut total_bytes = 0;
|
|
|
|
|
let mut ends_with_newline = true;
|
|
|
|
|
while let Some(chunk) = stream.next().await {
|
|
|
|
|
let chunk = ok_or_exit(chunk);
|
|
|
|
|
ends_with_newline = chunk.as_ref()[chunk.len() - 1] == b'\n';
|
|
|
|
|
total_bytes += chunk.len();
|
|
|
|
|
ok_or_exit(stdout.write(chunk.as_ref()).await);
|
|
|
|
|
}
|
|
|
|
|
if pretty && !ends_with_newline {
|
|
|
|
|
ok_or_exit(stdout.write(b"\x1b[1;97;101m%\x1b[0m\n").await);
|
|
|
|
|
}
|
|
|
|
|
total_bytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn content_type_is_json(headers: &HeaderMap) -> bool {
|
|
|
|
|
if let Some(content_type) = headers.get(CONTENT_TYPE) {
|
|
|
|
|
let content_type = content_type.as_bytes();
|
|
|
|
|
if !content_type.starts_with(b"application/") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let after_plus_pos = content_type
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.skip(12)
|
|
|
|
|
.find_map(|(i, &c)| (c == b'+').then_some(i + 1));
|
|
|
|
|
let after_plus = if let Some(plus_pos) = after_plus_pos {
|
|
|
|
|
&content_type[plus_pos..]
|
|
|
|
|
} else {
|
|
|
|
|
&content_type[12..]
|
|
|
|
|
};
|
|
|
|
|
after_plus.starts_with(b"json")
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn print_body_json(response: Response) -> usize {
|
|
|
|
|
let bytes = ok_or_exit(response.bytes().await);
|
|
|
|
|
let nbyte = bytes.len();
|
|
|
|
|
let s = ok_or_exit(String::from_utf8(bytes.to_vec()));
|
|
|
|
|
if let Ok(json) = json::parse(&s) {
|
|
|
|
|
let mut s = String::with_capacity(nbyte);
|
|
|
|
|
format_json(&json, &mut s, 0);
|
|
|
|
|
if !s.is_empty() {
|
|
|
|
|
s.push('\n');
|
|
|
|
|
}
|
|
|
|
|
ok_or_exit(stdout().write(s.as_bytes()).await);
|
|
|
|
|
} else {
|
|
|
|
|
ok_or_exit(stdout().write(s.as_bytes()).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nbyte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_json(json: &JsonValue, s: &mut String, indent: usize) {
|
|
|
|
|
match json {
|
|
|
|
|
JsonValue::Null => *s += "\x1b[0;96mnull\x1b[0m",
|
|
|
|
|
JsonValue::Boolean(b) => s.push_str(&format!("\x1b[0;96m{b}\x1b[0m")),
|
|
|
|
|
JsonValue::Number(n) => s.push_str(&format!("\x1b[0;33m{n}\x1b[0m")),
|
|
|
|
|
JsonValue::String(val) => s.push_str(&format!("\x1b[0;32m\"{val}\"\x1b[0m")),
|
|
|
|
|
JsonValue::Short(val) => s.push_str(&format!("\x1b[0;32m\"{val}\"\x1b[0m")),
|
|
|
|
|
JsonValue::Array(a) => {
|
|
|
|
|
if a.is_empty() {
|
|
|
|
|
s.push_str("[]");
|
|
|
|
|
} else {
|
|
|
|
|
s.push('[');
|
|
|
|
|
for (i, e) in a.iter().enumerate() {
|
|
|
|
|
let indent = indent + 4;
|
|
|
|
|
if i > 0 {
|
|
|
|
|
s.push(',');
|
|
|
|
|
}
|
|
|
|
|
s.push('\n');
|
|
|
|
|
s.extend((0..indent).into_iter().map(|_| ' '));
|
|
|
|
|
format_json(e, s, indent);
|
|
|
|
|
}
|
|
|
|
|
s.push('\n');
|
|
|
|
|
s.extend((0..indent).into_iter().map(|_| ' '));
|
|
|
|
|
s.push(']');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
JsonValue::Object(o) => {
|
|
|
|
|
if o.is_empty() {
|
|
|
|
|
s.push_str("{}");
|
|
|
|
|
} else {
|
|
|
|
|
s.push('{');
|
|
|
|
|
for (i, (k, v)) in o.iter().enumerate() {
|
|
|
|
|
let indent = indent + 4;
|
|
|
|
|
if i > 0 {
|
|
|
|
|
s.push(',');
|
|
|
|
|
}
|
|
|
|
|
s.push('\n');
|
|
|
|
|
s.extend((0..indent).into_iter().map(|_| ' '));
|
|
|
|
|
s.push_str(&format!("\x1b[0;36m\"{k}\"\x1b[0m: "));
|
|
|
|
|
format_json(v, s, indent);
|
|
|
|
|
}
|
|
|
|
|
s.push('\n');
|
|
|
|
|
s.extend((0..indent).into_iter().map(|_| ' '));
|
|
|
|
|
s.push('}');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn http_version(response: &Response) -> &'static str {
|
|
|
|
|
match response.version() {
|
|
|
|
|
Version::HTTP_09 => "0.9",
|
|
|
|
|