Got basic gemini listening working. Proxying does not yet work, nor does serving the origin
This commit is contained in:
parent
2d1e237735
commit
c8176c819f
@ -21,6 +21,10 @@ service:
|
||||
value: hi
|
||||
- name: user-agent
|
||||
value: ""
|
||||
gemini:
|
||||
proxied_domains:
|
||||
localhost:
|
||||
url: gemini://127.0.0.1:1965
|
||||
passphrase: foobar
|
||||
dns_records:
|
||||
- kind: A
|
||||
|
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -2963,9 +2963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
|
@ -20,7 +20,7 @@ serde_json = "1.0.96"
|
||||
trust-dns-client = "0.22.0"
|
||||
mockall = "0.11.4"
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.1", features = [ "full" ]}
|
||||
tokio = { version = "1.28.1", features = [ "full", "net" ]}
|
||||
signal-hook = "0.3.15"
|
||||
futures = "0.3.28"
|
||||
signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ]}
|
||||
|
@ -44,6 +44,8 @@
|
||||
pkgs.stdenv.cc
|
||||
pkgs.openssl
|
||||
toolchain
|
||||
|
||||
pkgs.gmni
|
||||
];
|
||||
shellHook = ''
|
||||
source $(pwd)/.env.dev
|
||||
|
@ -142,6 +142,12 @@ async fn main() {
|
||||
&mut task_stack,
|
||||
domain_manager.clone(),
|
||||
domain_manager.clone(),
|
||||
config.service.clone(),
|
||||
);
|
||||
|
||||
let _ = domani::service::gemini::Service::new(
|
||||
&mut task_stack,
|
||||
domain_manager.clone(),
|
||||
config.service,
|
||||
);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod config;
|
||||
pub mod gemini;
|
||||
pub mod http;
|
||||
|
||||
pub use config::*;
|
||||
|
@ -24,7 +24,7 @@ impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub passphrase: String,
|
||||
pub dns_records: Vec<ConfigDNSRecord>,
|
||||
@ -32,4 +32,6 @@ pub struct Config {
|
||||
pub primary_domain: domain::Name,
|
||||
#[serde(default)]
|
||||
pub http: service::http::Config,
|
||||
#[serde(default)]
|
||||
pub gemini: service::gemini::Config,
|
||||
}
|
||||
|
147
src/service/gemini.rs
Normal file
147
src/service/gemini.rs
Normal file
@ -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}")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
79
src/service/gemini/config.rs
Normal file
79
src/service/gemini/config.rs
Normal file
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -7,10 +7,10 @@ use serde_with::{serde_as, TryFromInto};
|
||||
use std::{collections, net, str::FromStr};
|
||||
|
||||
fn default_http_addr() -> net::SocketAddr {
|
||||
net::SocketAddr::from_str("[::]:3030").unwrap()
|
||||
net::SocketAddr::from_str("[::]:3080").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub enum ConfigFormMethod {
|
||||
GET,
|
||||
POST,
|
||||
@ -60,7 +60,7 @@ impl TryFrom<String> for ConfigProxiedDomainUrl {
|
||||
|
||||
fn try_from(url: String) -> Result<Self, Self::Error> {
|
||||
let parsed = http::Uri::from_str(url.as_str())
|
||||
.or_unexpected_while("parsing proxy url {proxy_url}")?;
|
||||
.map_unexpected_while(|| format!("parsing proxy url {url}"))?;
|
||||
|
||||
let scheme = parsed
|
||||
.scheme()
|
||||
@ -76,7 +76,7 @@ impl TryFrom<String> for ConfigProxiedDomainUrl {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct ConfigProxiedDomainRequestHeader {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
@ -129,7 +129,7 @@ impl TryFrom<Vec<ConfigProxiedDomainRequestHeader>> for ConfigProxiedDomainReque
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct ConfigProxiedDomain {
|
||||
#[serde_as(as = "TryFromInto<String>")]
|
||||
pub url: ConfigProxiedDomainUrl,
|
||||
@ -139,7 +139,7 @@ pub struct ConfigProxiedDomain {
|
||||
pub request_headers: ConfigProxiedDomainRequestHeaders,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_http_addr")]
|
||||
pub http_addr: net::SocketAddr,
|
||||
|
Loading…
Reference in New Issue
Block a user