initial commit uwu

This commit is contained in:
anna 2023-01-30 18:37:03 +01:00
commit dfe4676d2e
Signed by: fef
GPG key ID: EC22E476DC2D3D84
4 changed files with 1514 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1208
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "http-util"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = [ "derive" ] }
futures = "0.3"
reqwest = { version = "0.11", features = [ "blocking", "stream" ] }
tokio = { version = "1", features = [ "full" ] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

292
src/main.rs Normal file
View file

@ -0,0 +1,292 @@
use clap::{Parser, ValueEnum};
use futures::stream::StreamExt;
use reqwest::{
header::{HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE, LOCATION},
redirect, Body, RequestBuilder, Response, StatusCode, Url, Version,
};
use std::{env, fmt, os::fd::AsRawFd, process::exit, str::FromStr};
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Clone, Parser)]
#[command(name = "http")]
#[command(author, version, about = "HTTP(S) request utility")]
struct Params {
/// HTTP request method
method: RequestMethod,
/// URL (with or without protocol)
url: String,
/// Request body (`-` for stdin)
body: Option<String>,
/// Be verbose
#[arg(short = 'v', long)]
verbose: bool,
/// Follow redirects
#[arg(short = 'n', long)]
nofollow: bool,
/// Print HTTP status code
#[arg(short = 's')]
status: bool,
/// Additional request headers
#[arg(short = 'H')]
header: Option<Vec<String>>,
/// Show response headers
#[arg(short = 'i', long, conflicts_with = "headers")]
include_headers: bool,
/// Only show response headers
#[arg(short = 'I', long, conflicts_with = "include_headers")]
headers: bool,
/// Shortcut for setting the `Content-Type` header
#[arg(short = 't', long)]
content_type: Option<String>,
/// Shortcut for setting the `Accept` header
#[arg(short = 'a', long)]
accept: Option<String>,
}
#[derive(Debug, Copy, Clone, PartialEq, ValueEnum)]
enum RequestMethod {
Get,
Head,
Post,
Put,
Delete,
Options,
Patch,
}
impl From<RequestMethod> for reqwest::Method {
fn from(value: RequestMethod) -> reqwest::Method {
match value {
RequestMethod::Get => reqwest::Method::GET,
RequestMethod::Head => reqwest::Method::HEAD,
RequestMethod::Post => reqwest::Method::POST,
RequestMethod::Put => reqwest::Method::PUT,
RequestMethod::Delete => reqwest::Method::DELETE,
RequestMethod::Options => reqwest::Method::OPTIONS,
RequestMethod::Patch => reqwest::Method::PATCH,
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let mut params = Params::parse();
if params.method == RequestMethod::Options {
params.headers = true;
}
while let Some(next_url) = do_request(params.clone()).await {
if params.verbose {
eprintln!("Following redirect to {next_url}");
}
params.url = next_url;
}
}
async fn do_request(params: Params) -> Option<String> {
let client = ok_or_exit(
reqwest::ClientBuilder::new()
.redirect(redirect::Policy::none())
.build(),
);
let mut builder = client.request(params.method.into(), get_url(params.url));
if let Some(accept) = params.accept {
builder = builder.header(ACCEPT, accept);
}
if let Some(content_type) = params.content_type {
builder = builder.header(CONTENT_TYPE, content_type);
}
if let Some(headers) = params.header {
builder = append_headers(builder, &headers);
}
if let Some(body) = params.body {
builder = append_body(builder, body).await;
}
let response = ok_or_exit(builder.send().await);
let status = response.status();
if status.is_client_error() || status.is_server_error() {
eprintln!("HTTP/{} {}", http_version(&response), response.status());
exit(2);
}
let redirect_url = if status.is_redirection() && status != StatusCode::SEE_OTHER {
response
.headers()
.get(LOCATION)
.and_then(|location| String::from_utf8(Vec::from(location.as_bytes())).ok())
} else {
None
};
if redirect_url.is_none() {
if params.verbose {
println!("HTTP/{} {}", http_version(&response), response.status());
}
if params.include_headers || params.headers {
print_headers(&response);
println!();
}
if !params.headers {
let len = print_body(response).await;
if len == 0 && params.verbose {
eprintln!("Response body is empty");
}
}
}
redirect_url
}
async fn print_body(response: Response) -> usize {
let mut stdout = stdout();
let stream = response.bytes_stream();
tokio::pin!(stream);
let mut total_bytes = 0;
while let Some(chunk) = stream.next().await {
let chunk = ok_or_exit(chunk);
total_bytes += chunk.len();
ok_or_exit(stdout.write(chunk.as_ref()).await);
}
total_bytes
}
fn http_version(response: &Response) -> &'static str {
match response.version() {
Version::HTTP_09 => "0.9",
Version::HTTP_10 => "1.0",
Version::HTTP_11 => "1.1",
Version::HTTP_2 => "2",
Version::HTTP_3 => "3",
_ => "?",
}
}
fn print_headers(response: &Response) {
for (name, val) in response.headers() {
let (name, val) = (name.as_str(), val.as_bytes());
let val = match std::str::from_utf8(val) {
Ok(val) => val,
Err(e) => {
eprintln!("Error parsing header value for \"{name}\": {e}");
"???"
}
};
print_bold(name);
println!(": {val}");
}
}
fn get_url(url: String) -> Url {
let url = if !url.starts_with("http://") && !url.starts_with("https://") {
let arg0 = env::args().next();
if arg0.as_deref() == Some("https") {
format!("https://{url}")
} else {
format!("http://{url}")
}
} else {
url
};
ok_or_exit(url.parse())
}
fn append_headers(mut builder: RequestBuilder, headers: &[String]) -> RequestBuilder {
fn invalid_header(header: &str) -> String {
format!("Invalid header name: \"{header}\" is not in format \"name: value\"")
}
for header in headers {
let name_end = ok_or_exit(
header
.as_bytes()
.iter()
.enumerate()
.find_map(|(index, &c)| (c == b':' || c == b'=').then_some(index))
.ok_or_else(|| invalid_header(header)),
);
let name = &header[..name_end];
let val_start = ok_or_exit(
header
.as_bytes()
.iter()
.enumerate()
.skip(name_end)
.find_map(|(index, &c)| (!c.is_ascii_whitespace()).then_some(index))
.ok_or_else(|| invalid_header(header)),
);
let val = &header[val_start..];
let name = ok_or_exit(HeaderName::from_str(name));
let val = ok_or_exit(HeaderValue::from_str(val));
builder = builder.header(name, val);
}
builder
}
async fn append_body(builder: RequestBuilder, body: String) -> RequestBuilder {
let body = if body == "-" {
eprintln!("Reading body from stdin");
body_from_stdin().await
} else {
Body::from(body)
};
builder.body(body)
}
async fn body_from_stdin() -> Body {
let mut buf = Vec::new();
ok_or_exit(stdin().read_to_end(&mut buf).await);
Body::from(buf)
}
fn ok_or_exit<T, E>(result: Result<T, E>) -> T
where
E: fmt::Display,
{
match result {
Ok(val) => val,
Err(e) => {
eprintln!("{e}");
exit(1);
}
}
}
fn print_bold(s: &str) {
let stdout = stdout();
if isatty(stdout.as_raw_fd()) {
print!("\x1b[1m{s}\x1b[0m");
} else {
print!("{s}")
}
}
#[cfg(unix)]
fn isatty(fd: i32) -> bool {
let result = unsafe { libc::isatty(fd) };
result == 1
}
#[cfg(not(unix))]
fn isatty(_: i32) -> bool {
false
}