Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Jeidnx
Copy link
Contributor

@Jeidnx Jeidnx commented Nov 6, 2024

Description

This is a very crude proof of concept for implementing unix domain socket support. Currently only uds works and the path is hardcoded to /tmp/sws.sock. I wanted to have some feedback first, before fully implementing this. Here are a few points i would like to have feedback on / discuss:

  • How should the different server types be handled in the configuration? Currently there is a boolean to enable http2, but this wont work with three distinct types. A simple solution would be to replace the http2 boolean with a listen_type (or similar) String.
  • The start_server method is a bit too long imo, does it make sense to have seperate start functions for each type? On the other hand there is already some duplicate code in there, and splitting that into functions would make it harder to reuse code across listeners.

Related Issue

closes #484

Motivation and Context

See #484
I mainly want this because remembering paths is simpler than remembering ports, and for the performance.

How Has This Been Tested?

Tested functionality with curl, works as expected.

Screenshots (if appropriate):

@semanticdiff-com
Copy link

semanticdiff-com bot commented Nov 6, 2024

Review changes with  SemanticDiff

Changed Files
File Status
  src/server.rs  4% smaller
  src/transport.rs  0% smaller

@joseluisq joseluisq added enhancement New feature or request v2 v2 release labels Nov 6, 2024
@joseluisq
Copy link
Collaborator

  • How should the different server types be handled in the configuration? Currently there is a boolean to enable http2, but this wont work with three distinct types. A simple solution would be to replace the http2 boolean with a listen_type (or similar) String.

It can be the three types, but let me think a bit about it, maybe we do not need to do this. I will come back to this point soon.

  • The start_server method is a bit too long imo, does it make sense to have seperate start functions for each type? On the other hand there is already some duplicate code in there, and splitting that into functions would make it harder to reuse code across listeners.

Yes, that method is convoluted and we are aware of that. About this, there was an attempt to extract functionality and improve the code, for example, #377, but unfortunately got stuck.
It makes total sense to start moving out stuff to proper functions/modules and simplifying the code as well.

The question is, do you want to do it before UDS or after?

@Jeidnx
Copy link
Contributor Author

Jeidnx commented Nov 12, 2024

Server types

Im sure there doesn't need to be a server_type key. One option would be to create http_listener, http2_listener and socket_listener options which are mutually exclusive for example. Though im not sure how easy doing mutually exclusive options is with the current configuration file impl. I think having an explicit server_type is the easiest to implement and understand from a user perspective. I may have missed something though.

Refactoring start_server

I think it makes sense to do the refactoring together with adding uds support. The uds part isn't that big of a change i think so the changes should still be manageable.
My rough idea is to add the ServerType enum to the Server struct and turn it into a builder of some sorts. I should probably find some time later this week to really start working on this.

@joseluisq
Copy link
Collaborator

Im sure there doesn't need to be a server_type key. One option would be to create http_listener, http2_listener and socket_listener options which are mutually exclusive for example. Though im not sure how easy doing mutually exclusive options is with the current configuration file impl. I think having an explicit server_type is the easiest to implement and understand from a user perspective. I may have missed something though.

Honestly, It sounds fine to me. I also think this should allow us to decouple tls from http2 and deprecate --http2-cert/key in favor of more concise options like --tls, --tls-cert, --tls-key in the future (just an idea).

But keep in mind do not disrupt existing functionality and properly advise users to give them a proper path to migrate if the case.

I think it makes sense to do the refactoring together with adding uds support. The uds part isn't that big of a change i think so the changes should still be manageable.
My rough idea is to add the ServerType enum to the Server struct and turn it into a builder of some sorts. I should probably find some time later this week to really start working on this.

Feel free to go ahead. A prototype of this should be good to see.
Just take into account not to complicate yourself too much in the first phase. Maybe you want to split up the work in recessive PRs before or after UDS.

If you need any help just let me know.

@joseluisq
Copy link
Collaborator

What about an interface like this below that could convert from PathBuf::from(s) or s.parse::<IpAddr>()? (just an idea).

pub enum Interface {
    Address(IpAddr),
    Path(PathBuf),
}

Comment on lines +418 to +721
return match server_type {
ServerType::Http1(tcp_listener, addr_str) => {
tcp_listener
.set_nonblocking(true)
.with_context(|| "failed to set TCP non-blocking mode")?;

#[cfg(unix)]
let signals = signals::create_signals()
.with_context(|| "failed to register termination signals")?;
#[cfg(unix)]
let handle = signals.handle();
let http1_server = HyperServer::from_tcp(tcp_listener)
.unwrap()
.tcp_nodelay(true)
.serve(router_service);

let http2_server =
HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);
#[cfg(unix)]
let http1_cancel_recv = Arc::new(Mutex::new(_cancel_recv));

#[cfg(unix)]
let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
#[cfg(unix)]
let redirect_cancel_recv = http2_cancel_recv.clone();
#[cfg(unix)]
let http1_server = http1_server.with_graceful_shutdown(signals::wait_for_signals(
signals,
grace_period,
http1_cancel_recv,
));

#[cfg(unix)]
let http2_server = http2_server.with_graceful_shutdown(signals::wait_for_signals(
signals,
grace_period,
http2_cancel_recv,
));

#[cfg(windows)]
let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
#[cfg(windows)]
let redirect_cancel_recv = http2_cancel_recv.clone();

#[cfg(windows)]
let http2_ctrlc_recv = Arc::new(Mutex::new(Some(receiver)));
#[cfg(windows)]
let redirect_ctrlc_recv = http2_ctrlc_recv.clone();

#[cfg(windows)]
let http2_server = http2_server.with_graceful_shutdown(async move {
if general.windows_service {
signals::wait_for_ctrl_c(http2_cancel_recv, grace_period).await;
} else {
signals::wait_for_ctrl_c(http2_ctrlc_recv, grace_period).await;
}
});
#[cfg(windows)]
let http1_server = http1_server.with_graceful_shutdown(async move {
let http1_cancel_recv = if general.windows_service {
// http1_cancel_recv
Arc::new(Mutex::new(_cancel_recv))
} else {
// http1_ctrlc_recv
Arc::new(Mutex::new(Some(receiver)))
};
signals::wait_for_ctrl_c(http1_cancel_recv, grace_period).await;
});

server_info!(
parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
"http2 server is listening on https://{}",
addr_str
);
server_info!(
parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
"http1 server is listening on http://{}",
addr_str
);

server_info!("press ctrl+c to shut down the server");

#[cfg(unix)]
http1_server.await?;

// HTTP to HTTPS redirect server
if general.https_redirect {
let ip = general
.host
.parse::<IpAddr>()
.with_context(|| format!("failed to parse {} address", general.host))?;
let addr = SocketAddr::from((ip, general.https_redirect_from_port));
let tcp_listener = TcpListener::bind(addr)
.with_context(|| format!("failed to bind to {addr} address"))?;
#[cfg(windows)]
let http1_server_task = tokio::spawn(async move {
if let Err(err) = http1_server.await {
tracing::error!("http1 server failed to start up: {:?}", err);
std::process::exit(1)
}
});
#[cfg(windows)]
tokio::try_join!(ctrlc_task, http1_server_task)?;

#[cfg(windows)]
_cancel_fn();

#[cfg(unix)]
handle.close();

server_warn!("termination signal caught, shutting down the server execution");
Ok(())
}
#[cfg(feature = "http2")]
ServerType::Http2(tcp_listener, addr_str) => {
// HTTP to HTTPS redirect option
let https_redirect = general.https_redirect;
server_info!("http to https redirect: enabled={}", https_redirect);
server_info!(
parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
"http1 redirect server is listening on http://{}",
addr
"http to https redirect host: {}",
general.https_redirect_host
);
server_info!(
"http to https redirect from port: {}",
general.https_redirect_from_port
);
server_info!(
"http to https redirect from hosts: {}",
general.https_redirect_from_hosts
);

// HTTP/2 + TLS
tcp_listener
.set_nonblocking(true)
.with_context(|| "failed to set TCP non-blocking mode")?;
let listener = tokio::net::TcpListener::from_std(tcp_listener)
.with_context(|| "failed to create tokio::net::TcpListener")?;
let mut incoming = AddrIncoming::from_listener(listener).with_context(|| {
"failed to create an AddrIncoming from the current tokio::net::TcpListener"
})?;
incoming.set_nodelay(true);

let http2_tls_cert = match general.http2_tls_cert {
Some(v) => v,
_ => bail!("failed to initialize TLS because cert file missing"),
};
let http2_tls_key = match general.http2_tls_key {
Some(v) => v,
_ => bail!("failed to initialize TLS because key file missing"),
};

let tls = TlsConfigBuilder::new()
.cert_path(&http2_tls_cert)
.key_path(&http2_tls_key)
.build()
.with_context(|| {
"failed to initialize TLS probably because invalid cert or key file"
})?;

let http2_server =
HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);

#[cfg(unix)]
let redirect_signals = signals::create_signals()
.with_context(|| "failed to register termination signals")?;
let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
#[cfg(unix)]
let redirect_handle = redirect_signals.handle();

// Allowed redirect hosts
let redirect_allowed_hosts = general
.https_redirect_from_hosts
.split(',')
.map(|s| s.trim().to_owned())
.collect::<Vec<_>>();
if redirect_allowed_hosts.is_empty() {
bail!("https redirect allowed hosts is empty, provide at least one host or IP")
}
let redirect_cancel_recv = http2_cancel_recv.clone();

// Redirect options
let redirect_opts = Arc::new(https_redirect::RedirectOpts {
https_hostname: general.https_redirect_host,
https_port: general.port,
allowed_hosts: redirect_allowed_hosts,
});
#[cfg(unix)]
let http2_server = http2_server.with_graceful_shutdown(signals::wait_for_signals(
signals,
grace_period,
http2_cancel_recv,
));

let server_redirect = HyperServer::from_tcp(tcp_listener)
.unwrap()
.tcp_nodelay(true)
.serve(make_service_fn(move |_: &AddrStream| {
let redirect_opts = redirect_opts.clone();
let page404 = page404.clone();
let page50x = page50x.clone();
async move {
Ok::<_, error::Error>(service_fn(move |req| {
let redirect_opts = redirect_opts.clone();
let page404 = page404.clone();
let page50x = page50x.clone();
async move {
let uri = req.uri();
let method = req.method();
match https_redirect::redirect_to_https(&req, redirect_opts) {
Ok(resp) => Ok(resp),
Err(status) => error_page::error_response(
uri, method, &status, &page404, &page50x,
),
}
}
}))
}
}));
#[cfg(windows)]
let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
#[cfg(windows)]
let redirect_cancel_recv = http2_cancel_recv.clone();

#[cfg(windows)]
let http2_ctrlc_recv = Arc::new(Mutex::new(Some(receiver)));
#[cfg(windows)]
let redirect_ctrlc_recv = http2_ctrlc_recv.clone();

#[cfg(unix)]
let server_redirect = server_redirect.with_graceful_shutdown(
signals::wait_for_signals(redirect_signals, grace_period, redirect_cancel_recv),
);
#[cfg(windows)]
let server_redirect = server_redirect.with_graceful_shutdown(async move {
let http2_server = http2_server.with_graceful_shutdown(async move {
if general.windows_service {
signals::wait_for_ctrl_c(redirect_cancel_recv, grace_period).await;
signals::wait_for_ctrl_c(http2_cancel_recv, grace_period).await;
} else {
signals::wait_for_ctrl_c(redirect_ctrlc_recv, grace_period).await;
signals::wait_for_ctrl_c(http2_ctrlc_recv, grace_period).await;
}
});

// HTTP/2 server task
let server_task = tokio::spawn(async move {
if let Err(err) = http2_server.await {
tracing::error!("http2 server failed to start up: {:?}", err);
std::process::exit(1)
}
});
server_info!(
parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
"http2 server is listening on https://{}",
addr_str
);

// HTTP/1 redirect server task
let redirect_server_task = tokio::spawn(async move {
if let Err(err) = server_redirect.await {
tracing::error!("http1 redirect server failed to start up: {:?}", err);
std::process::exit(1)
// HTTP to HTTPS redirect server
if general.https_redirect {
let ip = general
.host
.parse::<IpAddr>()
.with_context(|| format!("failed to parse {} address", general.host))?;
let addr = SocketAddr::from((ip, general.https_redirect_from_port));
let tcp_listener = TcpListener::bind(addr)
.with_context(|| format!("failed to bind to {addr} address"))?;
server_info!(
parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
"http1 redirect server is listening on http://{}",
addr
);
tcp_listener
.set_nonblocking(true)
.with_context(|| "failed to set TCP non-blocking mode")?;

#[cfg(unix)]
let redirect_signals = signals::create_signals()
.with_context(|| "failed to register termination signals")?;
#[cfg(unix)]
let redirect_handle = redirect_signals.handle();

// Allowed redirect hosts
let redirect_allowed_hosts = general
.https_redirect_from_hosts
.split(',')
.map(|s| s.trim().to_owned())
.collect::<Vec<_>>();
if redirect_allowed_hosts.is_empty() {
bail!("https redirect allowed hosts is empty, provide at least one host or IP")
}
});

server_info!("press ctrl+c to shut down the servers");

#[cfg(windows)]
tokio::try_join!(ctrlc_task, server_task, redirect_server_task)?;
#[cfg(unix)]
tokio::try_join!(server_task, redirect_server_task)?;
// Redirect options
let redirect_opts = Arc::new(https_redirect::RedirectOpts {
https_hostname: general.https_redirect_host,
https_port: general.port,
allowed_hosts: redirect_allowed_hosts,
});

let server_redirect = HyperServer::from_tcp(tcp_listener)
.unwrap()
.tcp_nodelay(true)
.serve(make_service_fn(move |_: &AddrStream| {
let redirect_opts = redirect_opts.clone();
let page404 = page404.clone();
let page50x = page50x.clone();
async move {
Ok::<_, error::Error>(service_fn(move |req| {
let redirect_opts = redirect_opts.clone();
let page404 = page404.clone();
let page50x = page50x.clone();
async move {
let uri = req.uri();
let method = req.method();
match https_redirect::redirect_to_https(&req, redirect_opts)
{
Ok(resp) => Ok(resp),
Err(status) => error_page::error_response(
uri, method, &status, &page404, &page50x,
),
}
}
}))
}
}));

#[cfg(unix)]
let server_redirect =
server_redirect.with_graceful_shutdown(signals::wait_for_signals(
redirect_signals,
grace_period,
redirect_cancel_recv,
));
#[cfg(windows)]
let server_redirect = server_redirect.with_graceful_shutdown(async move {
if general.windows_service {
signals::wait_for_ctrl_c(redirect_cancel_recv, grace_period).await;
} else {
signals::wait_for_ctrl_c(redirect_ctrlc_recv, grace_period).await;
}
});

#[cfg(unix)]
redirect_handle.close();
} else {
server_info!("press ctrl+c to shut down the server");
http2_server.await?;
}
// HTTP/2 server task
let server_task = tokio::spawn(async move {
if let Err(err) = http2_server.await {
tracing::error!("http2 server failed to start up: {:?}", err);
std::process::exit(1)
}
});

#[cfg(unix)]
handle.close();
// HTTP/1 redirect server task
let redirect_server_task = tokio::spawn(async move {
if let Err(err) = server_redirect.await {
tracing::error!("http1 redirect server failed to start up: {:?}", err);
std::process::exit(1)
}
});

#[cfg(windows)]
_cancel_fn();
server_info!("press ctrl+c to shut down the servers");

server_warn!("termination signal caught, shutting down the server execution");
return Ok(());
}
#[cfg(windows)]
tokio::try_join!(ctrlc_task, server_task, redirect_server_task)?;
#[cfg(unix)]
tokio::try_join!(server_task, redirect_server_task)?;

// HTTP/1
#[cfg(unix)]
redirect_handle.close();
} else {
server_info!("press ctrl+c to shut down the server");
http2_server.await?;
}

#[cfg(unix)]
let signals =
signals::create_signals().with_context(|| "failed to register termination signals")?;
#[cfg(unix)]
let handle = signals.handle();
#[cfg(unix)]
handle.close();

tcp_listener
.set_nonblocking(true)
.with_context(|| "failed to set TCP non-blocking mode")?;
#[cfg(windows)]
_cancel_fn();

let http1_server = HyperServer::from_tcp(tcp_listener)
.unwrap()
.tcp_nodelay(true)
.serve(router_service);
server_warn!("termination signal caught, shutting down the server execution");
return Ok(());
}
#[cfg(unix)]
ServerType::Uds(unix_listener) => {
let addr = unix_listener.listener.local_addr()?;
let http1_server = HyperServer::builder(unix_listener).serve(router_service);
let http1_cancel_recv = Arc::new(Mutex::new(_cancel_recv));

#[cfg(unix)]
let http1_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
let http1_server = http1_server.with_graceful_shutdown(signals::wait_for_signals(
signals,
grace_period,
http1_cancel_recv,
));

#[cfg(unix)]
let http1_server = http1_server.with_graceful_shutdown(signals::wait_for_signals(
signals,
grace_period,
http1_cancel_recv,
));
server_info!(
parent: tracing::info_span!("Server::start_server", ?threads),
"http1 server is listening at {:?}",
addr
);

#[cfg(windows)]
let http1_server = http1_server.with_graceful_shutdown(async move {
let http1_cancel_recv = if general.windows_service {
// http1_cancel_recv
Arc::new(Mutex::new(_cancel_recv))
} else {
// http1_ctrlc_recv
Arc::new(Mutex::new(Some(receiver)))
};
signals::wait_for_ctrl_c(http1_cancel_recv, grace_period).await;
});
server_info!("press ctrl+c to shut down the server");

server_info!(
parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
"http1 server is listening on http://{}",
addr_str
);
http1_server.await?;

server_info!("press ctrl+c to shut down the server");
handle.close();

#[cfg(unix)]
http1_server.await?;
if let Some(path) = addr.as_pathname() {
tokio::fs::remove_file(path).await?;
}

#[cfg(windows)]
let http1_server_task = tokio::spawn(async move {
if let Err(err) = http1_server.await {
tracing::error!("http1 server failed to start up: {:?}", err);
std::process::exit(1)
server_warn!("termination signal caught, shutting down the server execution");
Ok(())
}
});
#[cfg(windows)]
tokio::try_join!(ctrlc_task, http1_server_task)?;

#[cfg(windows)]
_cancel_fn();

#[cfg(unix)]
handle.close();

server_warn!("termination signal caught, shutting down the server execution");
Ok(())
};

Check failure

Code scanning / clippy

unneeded return statement Error

unneeded return statement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request v2 v2 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Unix Domain Socket

2 participants