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