Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support http over unix domain sockets #392

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ba7774
reduce lifetime of printer closure passed to client middleware
ducaale Dec 29, 2024
aa8fbf5
support http over unix domain sockets
ducaale Dec 29, 2024
fe56317
implement test server for http_unix
ducaale Dec 29, 2024
531dba9
add initial tests for http_unix
ducaale Dec 29, 2024
bf4b1b8
fix clippy warnings
ducaale Dec 29, 2024
80242c2
disable http_unix tests in windows
ducaale Dec 29, 2024
8cee2fe
add middleware for managing cookies
ducaale Dec 30, 2024
652e3df
add test for cookies
ducaale Dec 30, 2024
b6d59d9
avoid mocking host header
ducaale Dec 30, 2024
cb8d396
use shortened version of cfg unix check
ducaale Dec 30, 2024
c7fb645
throw an error if unix-socket used in unsupported os
ducaale Dec 30, 2024
e0791b3
provide complete example for unix-socket usage
ducaale Dec 30, 2024
f09a191
fix missing import
ducaale Dec 30, 2024
67730e1
check that host header is passed
ducaale Dec 30, 2024
de52108
Merge branch 'master' into http-over-unix-socket
ducaale Dec 30, 2024
0a80407
store unix_client in ClientWithMiddleware
ducaale Dec 31, 2024
3d887f0
Merge branch 'master' into http-over-unix-socket
ducaale Jan 2, 2025
2eb6367
Merge branch 'master' into http-over-unix-socket
ducaale Jan 7, 2025
31c4420
warn or error if unix-socket used with unsupported option
ducaale Jan 7, 2025
8bcbb0d
disable failing badssl.com tests
ducaale Jan 7, 2025
d601946
implement read timeout for unix_socket requests
ducaale Jan 7, 2025
c84e8ef
implement connect timeout for unix_socket requests
ducaale Jan 8, 2025
e362e7f
switch from read timeout to total timeout
ducaale Jan 11, 2025
92df488
revert disabling badssl tests
ducaale Jan 11, 2025
bd8dbdd
Merge branch 'master' into http-over-unix-socket
ducaale Jan 11, 2025
7db9b38
Merge branch 'master' into http-over-unix-socket
ducaale Jan 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ dirs = "5.0"
encoding_rs = "0.8.28"
encoding_rs_io = "0.1.7"
flate2 = "1.0.22"
http = "1"
# Add "tracing" feature to hyper once it stabilizes
hyper = { version = "1.2", default-features = false }
hyper-util = { version = "0.1", features = ["tokio"] }
indicatif = "0.17"
jsonxf = "1.1.0"
memchr = "2.4.1"
Expand All @@ -47,6 +49,7 @@ serde_urlencoded = "0.7.0"
supports-hyperlinks = "3.0.0"
termcolor = "1.1.2"
time = "0.3.16"
tokio = { version = "1", features = ["rt-multi-thread"] }
unicode-width = "0.1.9"
url = "2.2.2"
ruzstd = { version = "0.7", default-features = false, features = ["std"]}
Expand Down
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ Example: --print=Hb"
#[clap(short = '6', long)]
pub ipv6: bool,

/// Connect using a Unix domain socket.
///
/// Example: --unix_socket=/var/run/temp.sock
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this API you still have to make up a hostname for the URL, like curl, and unlike got and the HTTPie plugin.

AFAICT the typical use cases are for local services and not for proxies, meaning that the hostname is superfluous. But it also doesn't look like we're going to get a URL-based convention for this any time soon. And splitting it off into an option prevents redirect attacks. (Not sure how it should interact with sessions though... we need to look carefully at other areas of interference.)

A full command with the : shorthand might help users, e.g.

xh :/index.html --unix-socket=/var/run/temp.sock

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a good chance that unix domain sockets will be treated as a proxy in reqwest. This has the advantage that anything relying on hostname like cookies and Host header will keep working. Also, see denoland/deno#8821 (comment)

However, I still think we can support xh http://unix:/var/run/docker.sock:/containers/json by treating it as a syntactic sugar for xh :/containers/json --unix-socket=/var/run/docker.sock.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always add syntactic sugar later. Right now there doesn't seem to be any consensus about the URL format.

If it ever gets to the point where browsers start supporting UDS URLs (as a non-experimental feature) then we should follow suit.

#[clap(long, value_name = "FILE")]
pub unix_socket: Option<PathBuf>,

/// Do not attempt to read stdin.
///
/// This disables the default behaviour of reading the request body from stdin
Expand Down
44 changes: 44 additions & 0 deletions src/cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::sync::Arc;

use anyhow::Result;
use reqwest::{
blocking::{Request, Response},
cookie::CookieStore,
header,
};

use crate::middleware::{Context, Middleware};

pub struct CookieMiddleware<T>(Arc<T>);

impl<T> CookieMiddleware<T> {
pub fn new(cookie_jar: Arc<T>) -> Self {
CookieMiddleware(cookie_jar)
}
}

impl<T: CookieStore> Middleware for CookieMiddleware<T> {
fn handle(&mut self, mut ctx: Context, mut request: Request) -> Result<Response> {
let url = request.url().clone();

if let Some(header) = self.0.cookies(&url) {
request
.headers_mut()
.entry(header::COOKIE)
.or_insert(header);
}

let response = self.next(&mut ctx, request)?;

let mut cookies = response
.headers()
.get_all(header::SET_COOKIE)
.iter()
.peekable();
if cookies.peek().is_some() {
self.0.set_cookies(&mut cookies, &url);
}

Ok(response)
}
}
74 changes: 42 additions & 32 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mod auth;
mod buffer;
mod cli;
mod cookie;
mod decoder;
mod download;
mod formatting;
Expand All @@ -14,6 +15,8 @@ mod redirect;
mod request_items;
mod session;
mod to_curl;
#[cfg(target_family = "unix")]
ducaale marked this conversation as resolved.
Show resolved Hide resolved
mod unix_socket;
mod utils;
mod vendored;

Expand All @@ -27,6 +30,7 @@ use std::str::FromStr;
use std::sync::Arc;

use anyhow::{anyhow, Context, Result};
use cookie::CookieMiddleware;
use cookie_store::{CookieStore, RawCookie};
use redirect::RedirectFollower;
use reqwest::blocking::Client;
Expand All @@ -35,7 +39,6 @@ use reqwest::header::{
};
use reqwest::tls;
use url::Host;
use utils::reason_phrase;

use crate::auth::{Auth, DigestAuthMiddleware};
use crate::buffer::Buffer;
Expand All @@ -45,7 +48,9 @@ use crate::middleware::ClientWithMiddleware;
use crate::printer::Printer;
use crate::request_items::{Body, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE};
use crate::session::Session;
use crate::utils::{test_mode, test_pretend_term, url_with_query};
#[cfg(target_family = "unix")]
use crate::unix_socket::UnixSocket;
use crate::utils::{reason_phrase, test_mode, test_pretend_term, url_with_query};
use crate::vendored::reqwest_cookie_store;

#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
Expand Down Expand Up @@ -282,9 +287,6 @@ fn run(args: Cli) -> Result<i32> {
None => client,
};

let cookie_jar = Arc::new(reqwest_cookie_store::CookieStoreMutex::default());
client = client.cookie_provider(cookie_jar.clone());

client = match (args.ipv4, args.ipv6) {
(true, false) => client.local_address(IpAddr::from(Ipv4Addr::UNSPECIFIED)),
(false, true) => client.local_address(IpAddr::from(Ipv6Addr::UNSPECIFIED)),
Expand Down Expand Up @@ -336,6 +338,8 @@ fn run(args: Cli) -> Result<i32> {
log::trace!("{client:#?}");
let client = client.build()?;

let cookie_jar = Arc::new(reqwest_cookie_store::CookieStoreMutex::default());

let mut session = match &args.session {
Some(name_or_path) => Some(
Session::load_session(url.clone(), name_or_path.clone(), args.is_session_read_only)
Expand Down Expand Up @@ -538,42 +542,48 @@ fn run(args: Cli) -> Result<i32> {
printer.print_request_body(&mut request)?;
}

let mut client = ClientWithMiddleware::new(client);

if !args.offline {
let mut response = {
let history_print = args.history_print.unwrap_or(print);
let mut client = ClientWithMiddleware::new(&client);
if args.all {
client = client.with_printer(|prev_response, next_request| {
if history_print.response_headers {
printer.print_response_headers(prev_response)?;
}
if history_print.response_body {
printer.print_response_body(
prev_response,
response_charset,
response_mime,
)?;
printer.print_separator()?;
}
if history_print.response_meta {
printer.print_response_meta(prev_response)?;
}
if history_print.request_headers {
printer.print_request_headers(next_request, &*cookie_jar)?;
}
if history_print.request_body {
printer.print_request_body(next_request)?;
}
Ok(())
});
}
if args.follow {
client = client.with(RedirectFollower::new(args.max_redirects.unwrap_or(10)));
}
if let Some(Auth::Digest(username, password)) = &auth {
client = client.with(DigestAuthMiddleware::new(username, password));
}
client.execute(request)?
client = client.with(CookieMiddleware::new(cookie_jar.clone()));
#[cfg(target_family = "unix")]
if let Some(unix_socket) = args.unix_socket {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a fork of hyperlocal that adds support for windows but it might be better to wait until it is merged upstream

client = client.with(UnixSocket::new(unix_socket));
}
#[cfg(not(target_family = "unix"))]
if let Some(_) = args.unix_socket {
log::warn!("HTTP over Unix domain sockets is not supported on this platform");
ducaale marked this conversation as resolved.
Show resolved Hide resolved
}
client.execute(request, |prev_response, next_request| {
if !args.all {
return Ok(());
}
if history_print.response_headers {
printer.print_response_headers(prev_response)?;
}
if history_print.response_body {
printer.print_response_body(prev_response, response_charset, response_mime)?;
printer.print_separator()?;
}
if history_print.response_meta {
printer.print_response_meta(prev_response)?;
}
if history_print.request_headers {
printer.print_request_headers(next_request, &*cookie_jar)?;
}
if history_print.request_body {
printer.print_request_body(next_request)?;
}
Ok(())
})?
};

let status = response.status();
Expand Down
54 changes: 18 additions & 36 deletions src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ impl ResponseExt for Response {
}
}

type Printer<'a, 'b> = &'a mut (dyn FnMut(&mut Response, &mut Request) -> Result<()> + 'b);
type Printer<'a> = &'a mut (dyn FnMut(&mut Response, &mut Request) -> Result<()> + 'a);

pub struct Context<'a, 'b> {
client: &'a Client,
printer: Option<Printer<'a, 'b>>,
pub struct Context<'a, 'b, 'c> {
client: Client,
printer: Printer<'c>,
middlewares: &'a mut [Box<dyn Middleware + 'b>],
}

impl<'a, 'b> Context<'a, 'b> {
impl<'a, 'b, 'c> Context<'a, 'b, 'c> {
fn new(
client: &'a Client,
printer: Option<Printer<'a, 'b>>,
client: Client,
printer: Printer<'c>,
middlewares: &'a mut [Box<dyn Middleware + 'b>],
) -> Self {
Context {
Expand All @@ -57,8 +57,7 @@ impl<'a, 'b> Context<'a, 'b> {
Ok(response)
}
[ref mut head, tail @ ..] => head.handle(
#[allow(clippy::needless_option_as_deref)]
Context::new(self.client, self.printer.as_deref_mut(), tail),
Context::new(self.client.clone(), self.printer, tail),
request,
),
}
Expand All @@ -78,51 +77,34 @@ pub trait Middleware {
response: &mut Response,
request: &mut Request,
) -> Result<()> {
if let Some(ref mut printer) = ctx.printer {
printer(response, request)?;
}

(ctx.printer)(response, request)?;
Ok(())
}
}

pub struct ClientWithMiddleware<'a, T>
where
T: FnMut(&mut Response, &mut Request) -> Result<()>,
{
client: &'a Client,
printer: Option<T>,
pub struct ClientWithMiddleware<'a> {
client: Client,
middlewares: Vec<Box<dyn Middleware + 'a>>,
}

impl<'a, T> ClientWithMiddleware<'a, T>
where
T: FnMut(&mut Response, &mut Request) -> Result<()> + 'a,
{
pub fn new(client: &'a Client) -> Self {
impl<'a> ClientWithMiddleware<'a> {
pub fn new(client: Client) -> Self {
ClientWithMiddleware {
client,
printer: None,
middlewares: vec![],
}
}

pub fn with_printer(mut self, printer: T) -> Self {
self.printer = Some(printer);
self
}

pub fn with(mut self, middleware: impl Middleware + 'a) -> Self {
self.middlewares.push(Box::new(middleware));
self
}

pub fn execute(&mut self, request: Request) -> Result<Response> {
let mut ctx = Context::new(
self.client,
self.printer.as_mut().map(|p| p as _),
&mut self.middlewares[..],
);
pub fn execute<'b, T>(&mut self, request: Request, mut printer: T) -> Result<Response>
where
T: FnMut(&mut Response, &mut Request) -> Result<()> + 'b,
{
let mut ctx = Context::new(self.client.clone(), &mut printer, &mut self.middlewares[..]);
ctx.execute(request)
}
}
6 changes: 2 additions & 4 deletions src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
formatting::serde_json_format,
formatting::{get_json_formatter, Highlighter},
middleware::ResponseExt,
utils::{copy_largebuf, test_mode, BUFFER_SIZE},
utils::{copy_largebuf, BUFFER_SIZE},
};

const BINARY_SUPPRESSOR: &str = concat!(
Expand Down Expand Up @@ -345,9 +345,7 @@ impl Printer {
// even know if we're going to use HTTP/2 yet.
headers.entry(HOST).or_insert_with(|| {
// Added at https://github.com/hyperium/hyper-util/blob/53aadac50d/src/client/legacy/client.rs#L278
if test_mode() {
HeaderValue::from_str("http.mock")
} else if let Some(port) = request.url().port() {
if let Some(port) = request.url().port() {
HeaderValue::from_str(&format!("{}:{}", host, port))
} else {
HeaderValue::from_str(host)
Expand Down
5 changes: 5 additions & 0 deletions src/to_curl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ pub fn translate(args: Cli) -> Result<Command> {
cmd.arg(interface);
};

if let Some(unix_socket) = args.unix_socket {
cmd.arg("--unix-socket");
cmd.arg(unix_socket);
}

if !args.resolve.is_empty() {
let port = url
.port_or_known_default()
Expand Down
Loading
Loading