parent
2d1e237735
commit
c8176c819f
@ -1,4 +1,5 @@ |
||||
mod config; |
||||
pub mod gemini; |
||||
pub mod http; |
||||
|
||||
pub use config::*; |
||||
|
@ -0,0 +1,147 @@ |
||||
mod config; |
||||
|
||||
pub use config::*; |
||||
|
||||
use crate::error::unexpected::{self, Mappable}; |
||||
use crate::{domain, service, task_stack}; |
||||
|
||||
use std::sync; |
||||
use tokio_util::sync::CancellationToken; |
||||
|
||||
pub struct Service { |
||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, |
||||
config: service::Config, |
||||
} |
||||
|
||||
#[derive(thiserror::Error, Debug)] |
||||
enum HandleConnError { |
||||
#[error("client error: {0}")] |
||||
ClientError(String), |
||||
|
||||
#[error(transparent)] |
||||
Unexpected(#[from] unexpected::Error), |
||||
} |
||||
|
||||
impl Service { |
||||
pub fn new( |
||||
task_stack: &mut task_stack::TaskStack<unexpected::Error>, |
||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, |
||||
config: service::Config, |
||||
) -> sync::Arc<Service> { |
||||
let service = sync::Arc::new(Service { |
||||
cert_resolver, |
||||
config, |
||||
}); |
||||
task_stack.push_spawn(|canceller| listen(service.clone(), canceller)); |
||||
service |
||||
} |
||||
|
||||
async fn proxy_conn( |
||||
&self, |
||||
proxied_domain: &ConfigProxiedDomain, |
||||
mut conn: tokio::net::TcpStream, |
||||
) -> unexpected::Result<()> { |
||||
let mut proxy_conn = tokio::net::TcpStream::connect(&proxied_domain.url.addr) |
||||
.await |
||||
.map_unexpected_while(|| { |
||||
format!("failed to connect to proxy {}", proxied_domain.url.url,) |
||||
})?; |
||||
|
||||
_ = tokio::io::copy_bidirectional(&mut conn, &mut proxy_conn).await; |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
async fn handle_conn( |
||||
&self, |
||||
conn: tokio::net::TcpStream, |
||||
tls_conn: rustls::ServerConnection, |
||||
) -> Result<(), HandleConnError> { |
||||
let acceptor = |
||||
tokio_rustls::LazyConfigAcceptor::new(rustls::server::Acceptor::default(), conn); |
||||
futures::pin_mut!(acceptor); |
||||
|
||||
match acceptor.as_mut().await { |
||||
Ok(start) => { |
||||
let client_hello = start.client_hello(); |
||||
|
||||
let domain = client_hello.server_name().ok_or_else(|| { |
||||
HandleConnError::ClientError("missing SNI in ClientHello".to_string()) |
||||
})?; |
||||
|
||||
let domain: domain::Name = domain.parse().map_err(|e| { |
||||
HandleConnError::ClientError(format!( |
||||
"parsing domain {domain}, provided in SNI: {e}" |
||||
)) |
||||
})?; |
||||
|
||||
// If the domain should be proxied, then proxy it
|
||||
if let Some(proxied_domain) = self.config.gemini.proxied_domains.get(&domain) { |
||||
let conn = acceptor |
||||
.take_io() |
||||
.expect("failed to take back underlying TCP connection"); |
||||
|
||||
self.proxy_conn(proxied_domain, conn).await?; |
||||
return Ok(()); |
||||
} |
||||
|
||||
return Err(HandleConnError::ClientError(format!( |
||||
"unknown domain {domain}" |
||||
))); |
||||
} |
||||
Err(err) => { |
||||
return Err(unexpected::Error::from( |
||||
format!("failed to accept TLS connection: {err}").as_str(), |
||||
) |
||||
.into()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn listen( |
||||
service: sync::Arc<Service>, |
||||
canceller: CancellationToken, |
||||
) -> unexpected::Result<()> { |
||||
let tls_config = sync::Arc::new( |
||||
rustls::server::ServerConfig::builder() |
||||
.with_safe_defaults() |
||||
.with_no_client_auth() // TODO maybe this isn't right?
|
||||
.with_cert_resolver(service.cert_resolver.clone()), |
||||
); |
||||
|
||||
log::info!( |
||||
"Listening on gemini://{}:{}", |
||||
&service.config.primary_domain.clone(), |
||||
&service.config.gemini.gemini_addr.port(), |
||||
); |
||||
|
||||
let listener = tokio::net::TcpListener::bind(service.config.gemini.gemini_addr) |
||||
.await |
||||
.or_unexpected_while("binding tcp socket")?; |
||||
|
||||
loop { |
||||
let (conn, addr) = tokio::select! { |
||||
res = listener.accept() => res.or_unexpected_while("accepting connection")?, |
||||
_ = canceller.cancelled() => return Ok(()), |
||||
}; |
||||
|
||||
let service = service.clone(); |
||||
let tls_config = tls_config.clone(); |
||||
|
||||
tokio::spawn(async move { |
||||
let tls_conn = rustls::ServerConnection::new(tls_config) |
||||
.expect("failed to initialize TLS connection state"); |
||||
|
||||
match service.handle_conn(conn, tls_conn).await { |
||||
Ok(_) => (), |
||||
Err(HandleConnError::ClientError(e)) => { |
||||
log::warn!("Bad request from connection {addr}: {e}") |
||||
} |
||||
Err(HandleConnError::Unexpected(e)) => { |
||||
log::error!("Server error handling connection {addr}: {e}") |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
use crate::domain; |
||||
use crate::error::unexpected::{self, Mappable}; |
||||
|
||||
use serde::{Deserialize, Serialize}; |
||||
use serde_with::{serde_as, TryFromInto}; |
||||
|
||||
use std::{collections, net, str::FromStr}; |
||||
|
||||
fn default_gemini_addr() -> net::SocketAddr { |
||||
net::SocketAddr::from_str("[::]:3965").unwrap() |
||||
} |
||||
|
||||
#[derive(Deserialize, Serialize, Clone)] |
||||
pub struct ConfigProxiedDomainUrl { |
||||
pub url: String, |
||||
pub addr: String, |
||||
} |
||||
|
||||
impl From<ConfigProxiedDomainUrl> for String { |
||||
fn from(url: ConfigProxiedDomainUrl) -> Self { |
||||
url.url |
||||
} |
||||
} |
||||
|
||||
impl TryFrom<String> for ConfigProxiedDomainUrl { |
||||
type Error = unexpected::Error; |
||||
|
||||
fn try_from(url: String) -> Result<Self, Self::Error> { |
||||
// use http's implementation, should be the same
|
||||
let parsed = http::Uri::from_str(url.as_str()) |
||||
.map_unexpected_while(|| format!("parsing proxy url {url}"))?; |
||||
|
||||
let scheme = parsed.scheme().map_unexpected_while(|| { |
||||
format!("expected a scheme of gemini in the proxy url {url}") |
||||
})?; |
||||
|
||||
if scheme != "gemini" { |
||||
return Err(unexpected::Error::from( |
||||
format!("scheme of proxy url {url} should be 'gemini'",).as_str(), |
||||
)); |
||||
} |
||||
|
||||
match parsed.authority() { |
||||
None => Err(unexpected::Error::from( |
||||
format!("proxy url {url} should have a host",).as_str(), |
||||
)), |
||||
Some(authority) => { |
||||
let port = authority.port().map(|p| p.as_u16()).unwrap_or(1965); |
||||
Ok(ConfigProxiedDomainUrl { |
||||
url: url, |
||||
addr: format!("{}:{port}", authority.host()), |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[serde_as] |
||||
#[derive(Deserialize, Serialize, Clone)] |
||||
pub struct ConfigProxiedDomain { |
||||
#[serde_as(as = "TryFromInto<String>")] |
||||
pub url: ConfigProxiedDomainUrl, |
||||
} |
||||
|
||||
#[derive(Deserialize, Serialize, Clone)] |
||||
pub struct Config { |
||||
#[serde(default = "default_gemini_addr")] |
||||
pub gemini_addr: net::SocketAddr, |
||||
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>, |
||||
} |
||||
|
||||
impl Default for Config { |
||||
fn default() -> Self { |
||||
Self { |
||||
gemini_addr: default_gemini_addr(), |
||||
proxied_domains: Default::default(), |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue