Support for gemini fully fleshed out
This commit is contained in:
parent
d429b51cf8
commit
96b38f2c97
100
Cargo.lock
generated
100
Cargo.lock
generated
@ -111,6 +111,12 @@ version = "1.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@ -158,6 +164,18 @@ version = "2.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "0.19.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -458,9 +476,11 @@ name = "domani"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acme2",
|
"acme2",
|
||||||
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
|
"gemini",
|
||||||
"gix",
|
"gix",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
"hex",
|
"hex",
|
||||||
@ -660,6 +680,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.28"
|
version = "0.3.28"
|
||||||
@ -749,6 +775,18 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gemini"
|
||||||
|
version = "0.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12a820f5a9ac6f433b34944dc8d17b759d5009275c8fe12f73b873153dbcd4e0"
|
||||||
|
dependencies = [
|
||||||
|
"nom 6.1.2",
|
||||||
|
"paste",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -828,7 +866,7 @@ dependencies = [
|
|||||||
"btoi",
|
"btoi",
|
||||||
"gix-date",
|
"gix-date",
|
||||||
"itoa",
|
"itoa",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -891,7 +929,7 @@ dependencies = [
|
|||||||
"gix-sec",
|
"gix-sec",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@ -1100,7 +1138,7 @@ dependencies = [
|
|||||||
"gix-validate",
|
"gix-validate",
|
||||||
"hex",
|
"hex",
|
||||||
"itoa",
|
"itoa",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@ -1195,7 +1233,7 @@ dependencies = [
|
|||||||
"gix-hash",
|
"gix-hash",
|
||||||
"gix-transport",
|
"gix-transport",
|
||||||
"maybe-async",
|
"maybe-async",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1226,7 +1264,7 @@ dependencies = [
|
|||||||
"gix-tempfile",
|
"gix-tempfile",
|
||||||
"gix-validate",
|
"gix-validate",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1767,6 +1805,19 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexical-core"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec 0.5.2",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.142"
|
version = "0.2.142"
|
||||||
@ -1954,6 +2005,19 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "6.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
|
||||||
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"funty",
|
||||||
|
"lexical-core",
|
||||||
|
"memchr",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@ -2081,6 +2145,12 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -2249,6 +2319,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radix_trie"
|
name = "radix_trie"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -2805,6 +2881,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempdir"
|
name = "tempdir"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@ -3121,7 +3203,7 @@ version = "3.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "794a32261a1f5eb6a4462c81b59cec87b5c27d5deea7dd1ac8fc781c41d226db"
|
checksum = "794a32261a1f5eb6a4462c81b59cec87b5c27d5deea7dd1ac8fc781c41d226db"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec 0.7.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3508,3 +3590,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
||||||
|
@ -31,7 +31,7 @@ mime_guess = "2.0.4"
|
|||||||
hyper = { version = "0.14.26", features = [ "server", "stream" ]}
|
hyper = { version = "0.14.26", features = [ "server", "stream" ]}
|
||||||
http = "0.2.9"
|
http = "0.2.9"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
tokio-util = "0.7.8"
|
tokio-util = { version = "0.7.8", features = [ "io" ]}
|
||||||
acme2 = "0.5.1"
|
acme2 = "0.5.1"
|
||||||
openssl = "0.10.52"
|
openssl = "0.10.52"
|
||||||
rustls = "0.21.1"
|
rustls = "0.21.1"
|
||||||
@ -45,6 +45,8 @@ serde_yaml = "0.9.22"
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
reqwest = "0.11.18"
|
reqwest = "0.11.18"
|
||||||
hyper-reverse-proxy = "0.5.1"
|
hyper-reverse-proxy = "0.5.1"
|
||||||
|
gemini = "0.0.5"
|
||||||
|
bytes = "1.4.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
tokio-rustls = { git = "https://code.betamike.com/micropelago/tokio-rustls.git", branch = "start-handshake-into-inner" }
|
tokio-rustls = { git = "https://code.betamike.com/micropelago/tokio-rustls.git", branch = "start-handshake-into-inner" }
|
||||||
|
65
README.md
65
README.md
@ -7,6 +7,17 @@ their DNS server.
|
|||||||
|
|
||||||
[Demo which may or may not be live](https://domani.mediocregopher.com)
|
[Demo which may or may not be live](https://domani.mediocregopher.com)
|
||||||
|
|
||||||
|
Domani supports serving domains using the following protocols:
|
||||||
|
|
||||||
|
- HTTP
|
||||||
|
- HTTPS (with SSL certificates automatically retrieved using LetsEncrypt)
|
||||||
|
- [Gemini](https://gemini.circumlunar.space/)
|
||||||
|
|
||||||
|
Files are served as-is, with their extension being used to determine
|
||||||
|
Content-Type. If a directory is requested (e.g. `/some/dir/`) then `index.html`
|
||||||
|
will be requested if the protocol is HTTP, or `index.gmi` if the protocol is
|
||||||
|
gemini.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Domani uses nix flakes for building and setting up the development environment.
|
Domani uses nix flakes for building and setting up the development environment.
|
||||||
@ -60,13 +71,13 @@ domain:
|
|||||||
|
|
||||||
# An example built-in domain backed by a git repo.
|
# An example built-in domain backed by a git repo.
|
||||||
#git.example.com:
|
#git.example.com:
|
||||||
# kind: git
|
#kind: git
|
||||||
# url: "https://somewhere.com/some/repo.git"
|
#url: "https://somewhere.com/some/repo.git"
|
||||||
# branch_name: main
|
#branch_name: main
|
||||||
#
|
|
||||||
# # If true then the built-in will be included in the web interface's
|
# If true then the built-in will be included in the web interface's
|
||||||
# # domain list, but will not be configurable in the web interface
|
# domain list, but will not be configurable in the web interface
|
||||||
# public: false
|
#public: false
|
||||||
|
|
||||||
service:
|
service:
|
||||||
|
|
||||||
@ -102,8 +113,9 @@ service:
|
|||||||
# https_addr is set.
|
# https_addr is set.
|
||||||
#http_addr: "[::]:3080"
|
#http_addr: "[::]:3080"
|
||||||
|
|
||||||
# The address to listen for HTTPS requests on. This is optional.
|
# The address to listen for HTTPS requests on. Defaults to not having HTTP
|
||||||
#https_addr: "[::]:443"
|
# enabled. You can enable HTTPS by setting this to "[::]:443".
|
||||||
|
#https_addr: null
|
||||||
|
|
||||||
#proxied_domains:
|
#proxied_domains:
|
||||||
|
|
||||||
@ -117,7 +129,6 @@ service:
|
|||||||
# * dns.resolver_addr is ignored and the system-wide dns is used
|
# * dns.resolver_addr is ignored and the system-wide dns is used
|
||||||
#
|
#
|
||||||
#proxy.example.com:
|
#proxy.example.com:
|
||||||
# kind: http_proxy
|
|
||||||
# url: "http://some.other.service.com"
|
# url: "http://some.other.service.com"
|
||||||
#
|
#
|
||||||
# # Extra headers to add to proxied requests
|
# # Extra headers to add to proxied requests
|
||||||
@ -126,6 +137,25 @@ service:
|
|||||||
# value: "yet.another.service.com"
|
# value: "yet.another.service.com"
|
||||||
# - name: X-HEADER-TO-DELETE
|
# - name: X-HEADER-TO-DELETE
|
||||||
# value: ""
|
# value: ""
|
||||||
|
|
||||||
|
#gemini:
|
||||||
|
|
||||||
|
# The address to listen for gemini requests on. Set this to null to disable
|
||||||
|
# gemini support.
|
||||||
|
#gemini_addr: "[::]:3965"
|
||||||
|
|
||||||
|
#proxied_domains:
|
||||||
|
|
||||||
|
# An example built-in domain backed by a reverse-proxy to some other
|
||||||
|
# gemini server. Requests to this domain will have connections
|
||||||
|
# transparently proxied to the backing server.
|
||||||
|
#
|
||||||
|
# Proxies are currently limited in the following ways:
|
||||||
|
# * url must be to a gemini endpoint
|
||||||
|
# * dns.resolver_addr is ignored and the system-wide dns is used
|
||||||
|
#
|
||||||
|
#proxy.example.com:
|
||||||
|
# url: "gemini://some.other.service.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
The YAML config file can be passed to the Domani process via the `--config-path`
|
The YAML config file can be passed to the Domani process via the `--config-path`
|
||||||
@ -159,10 +189,25 @@ nix develop
|
|||||||
|
|
||||||
Within the shell which opens you can do `cargo run` to start a local instance.
|
Within the shell which opens you can do `cargo run` to start a local instance.
|
||||||
|
|
||||||
|
Using the default configuration, the domain `domani-test.localhost` should be
|
||||||
|
immediately available at:
|
||||||
|
|
||||||
|
* `http://domani-test.localhost:3080`
|
||||||
|
* `gemini://domani-test.localhost:3965`
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
* Better web interface design.
|
||||||
|
* Tutorials aimed at beginner users.
|
||||||
|
|
||||||
* Support for more backends than just git repositories, including:
|
* Support for more backends than just git repositories, including:
|
||||||
* IPFS/IPNS
|
* IPFS/IPNS
|
||||||
* Small static files (e.g. for well-knowns)
|
* Small static files (e.g. for well-knowns)
|
||||||
* Google Drive
|
* Google Drive
|
||||||
* Dropbox
|
* Dropbox
|
||||||
|
|
||||||
|
* Automatic HTTP/gemtext rendering for markdown files.
|
||||||
|
* Automatic HTTP rendering for gemtext files.
|
||||||
|
|
||||||
|
* Ability to disable the web interface.
|
||||||
|
* Ability to disable HTTP completely.
|
||||||
|
@ -4,7 +4,21 @@ use crate::{domain, util};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::{fs, path, sync};
|
use std::{fs, path};
|
||||||
|
|
||||||
|
pub trait Store {
|
||||||
|
fn get_certificate(
|
||||||
|
&self,
|
||||||
|
domain: &domain::Name,
|
||||||
|
) -> unexpected::Result<Option<(PrivateKey, Certificate)>>;
|
||||||
|
|
||||||
|
fn set_certificate(
|
||||||
|
&self,
|
||||||
|
domain: &domain::Name,
|
||||||
|
pkey: PrivateKey,
|
||||||
|
cert: Certificate,
|
||||||
|
) -> unexpected::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct StoredPKeyCert {
|
struct StoredPKeyCert {
|
||||||
@ -30,11 +44,13 @@ impl FSStore {
|
|||||||
domain.push_str(".json");
|
domain.push_str(".json");
|
||||||
self.cert_dir_path.join(domain)
|
self.cert_dir_path.join(domain)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store for FSStore {
|
||||||
fn get_certificate(
|
fn get_certificate(
|
||||||
&self,
|
&self,
|
||||||
domain: &domain::Name,
|
domain: &domain::Name,
|
||||||
) -> unexpected::Result<(PrivateKey, Certificate)> {
|
) -> unexpected::Result<Option<(PrivateKey, Certificate)>> {
|
||||||
let path = self.pkey_cert_path(domain);
|
let path = self.pkey_cert_path(domain);
|
||||||
|
|
||||||
let file = match util::open_file(path.as_path())
|
let file = match util::open_file(path.as_path())
|
||||||
@ -42,60 +58,34 @@ impl FSStore {
|
|||||||
{
|
{
|
||||||
Some(file) => file,
|
Some(file) => file,
|
||||||
None => {
|
None => {
|
||||||
let pkey = PrivateKey::new();
|
return Ok(None);
|
||||||
let cert = Certificate::new_self_signed(&pkey, domain)
|
|
||||||
.or_unexpected_while("creating self-signed cert")?;
|
|
||||||
|
|
||||||
let file = fs::File::create(path.as_path())
|
|
||||||
.map_unexpected_while(|| format!("creating file {}", path.display()))?;
|
|
||||||
|
|
||||||
let stored = StoredPKeyCert {
|
|
||||||
private_key: pkey,
|
|
||||||
cert,
|
|
||||||
};
|
|
||||||
|
|
||||||
serde_json::to_writer(file, &stored).or_unexpected_while("writing cert to file")?;
|
|
||||||
|
|
||||||
return Ok((stored.private_key, stored.cert));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stored: StoredPKeyCert =
|
let stored: StoredPKeyCert =
|
||||||
serde_json::from_reader(file).or_unexpected_while("parsing json")?;
|
serde_json::from_reader(file).or_unexpected_while("parsing json")?;
|
||||||
|
|
||||||
Ok((stored.private_key, stored.cert))
|
Ok(Some((stored.private_key, stored.cert)))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl rustls::server::ResolvesServerCert for FSStore {
|
fn set_certificate(
|
||||||
fn resolve(
|
|
||||||
&self,
|
&self,
|
||||||
client_hello: rustls::server::ClientHello<'_>,
|
domain: &domain::Name,
|
||||||
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
pkey: PrivateKey,
|
||||||
let domain = client_hello.server_name()?;
|
cert: Certificate,
|
||||||
|
) -> unexpected::Result<()> {
|
||||||
|
let path = self.pkey_cert_path(domain);
|
||||||
|
|
||||||
let res: unexpected::Result<Option<sync::Arc<rustls::sign::CertifiedKey>>> = (|| {
|
let file = fs::File::create(path.as_path())
|
||||||
let domain: domain::Name = domain
|
.map_unexpected_while(|| format!("creating file {}", path.display()))?;
|
||||||
.parse()
|
|
||||||
.map_unexpected_while(|| format!("parsing domain {domain}"))?;
|
|
||||||
|
|
||||||
let (pkey, cert) = self
|
let stored = StoredPKeyCert {
|
||||||
.get_certificate(&domain)
|
private_key: pkey,
|
||||||
.or_unexpected_while("fetching pkey/cert")?;
|
cert,
|
||||||
|
};
|
||||||
|
|
||||||
let pkey = rustls::sign::any_supported_type(&pkey.into()).or_unexpected()?;
|
serde_json::to_writer(file, &stored).or_unexpected_while("writing cert to file")?;
|
||||||
|
|
||||||
Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey {
|
return Ok(());
|
||||||
cert: vec![cert.into()],
|
|
||||||
key: pkey,
|
|
||||||
ocsp: None,
|
|
||||||
sct_list: None,
|
|
||||||
})))
|
|
||||||
})();
|
|
||||||
|
|
||||||
res.unwrap_or_else(|err| {
|
|
||||||
log::error!("Unexpected error getting cert for domain {domain}: {err}");
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::domain::{self, acme, checker, store};
|
use crate::domain::{self, acme, checker, gemini, store, tls};
|
||||||
use crate::error::unexpected::{self, Mappable};
|
use crate::error::unexpected::{self, Mappable};
|
||||||
use crate::{origin, task_stack, util};
|
use crate::{origin, task_stack, util};
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ impl From<store::SetError> for SyncWithSettingsError {
|
|||||||
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
//#[mockall::automock]
|
//#[mockall::automock]
|
||||||
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
pub trait Manager: Sync + Send {
|
||||||
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError>;
|
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError>;
|
||||||
|
|
||||||
fn get_file<'store>(
|
fn get_file<'store>(
|
||||||
@ -178,6 +178,7 @@ pub struct ManagerImpl {
|
|||||||
domain_store: Box<dyn store::Store + Send + Sync>,
|
domain_store: Box<dyn store::Store + Send + Sync>,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
|
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
|
||||||
|
gemini_store: Option<Box<dyn gemini::Store + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagerImpl {
|
impl ManagerImpl {
|
||||||
@ -185,12 +186,14 @@ impl ManagerImpl {
|
|||||||
OriginStore: origin::Store + Send + Sync + 'static,
|
OriginStore: origin::Store + Send + Sync + 'static,
|
||||||
DomainStore: store::Store + Send + Sync + 'static,
|
DomainStore: store::Store + Send + Sync + 'static,
|
||||||
AcmeManager: acme::manager::Manager + Send + Sync + 'static,
|
AcmeManager: acme::manager::Manager + Send + Sync + 'static,
|
||||||
|
GeminiStore: gemini::Store + Send + Sync + 'static,
|
||||||
>(
|
>(
|
||||||
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
||||||
origin_store: OriginStore,
|
origin_store: OriginStore,
|
||||||
domain_store: DomainStore,
|
domain_store: DomainStore,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<AcmeManager>,
|
acme_manager: Option<AcmeManager>,
|
||||||
|
gemini_store: Option<GeminiStore>,
|
||||||
) -> sync::Arc<Self> {
|
) -> sync::Arc<Self> {
|
||||||
let manager = sync::Arc::new(ManagerImpl {
|
let manager = sync::Arc::new(ManagerImpl {
|
||||||
origin_store: Box::from(origin_store),
|
origin_store: Box::from(origin_store),
|
||||||
@ -198,6 +201,7 @@ impl ManagerImpl {
|
|||||||
domain_checker,
|
domain_checker,
|
||||||
acme_manager: acme_manager
|
acme_manager: acme_manager
|
||||||
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
|
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
|
||||||
|
gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>),
|
||||||
});
|
});
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| {
|
task_stack.push_spawn(|canceller| {
|
||||||
@ -248,6 +252,23 @@ impl ManagerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_gemini_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
|
||||||
|
if let Some(ref gemini_store) = self.gemini_store {
|
||||||
|
if let Some(_) = gemini_store.get_certificate(domain).or_unexpected()? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// no cert/key stored for the domain, generate and store it
|
||||||
|
let pkey = tls::PrivateKey::new();
|
||||||
|
let cert = tls::Certificate::new_self_signed(&pkey, domain)
|
||||||
|
.or_unexpected_while("creating self-signed cert")?;
|
||||||
|
|
||||||
|
gemini_store.set_certificate(domain, pkey, cert)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manager for ManagerImpl {
|
impl Manager for ManagerImpl {
|
||||||
@ -300,6 +321,8 @@ impl Manager for ManagerImpl {
|
|||||||
|
|
||||||
self.domain_store.set(&domain, &settings)?;
|
self.domain_store.set(&domain, &settings)?;
|
||||||
|
|
||||||
|
self.sync_gemini_cert(&domain)?;
|
||||||
|
|
||||||
self.sync_https_cert(domain).await?;
|
self.sync_https_cert(domain).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -329,14 +352,22 @@ impl Manager for ManagerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl rustls::server::ResolvesServerCert for ManagerImpl {
|
pub struct HttpsCertResolver(sync::Arc<ManagerImpl>);
|
||||||
|
|
||||||
|
impl From<sync::Arc<ManagerImpl>> for HttpsCertResolver {
|
||||||
|
fn from(mgr: sync::Arc<ManagerImpl>) -> Self {
|
||||||
|
Self(mgr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustls::server::ResolvesServerCert for HttpsCertResolver {
|
||||||
fn resolve(
|
fn resolve(
|
||||||
&self,
|
&self,
|
||||||
client_hello: rustls::server::ClientHello<'_>,
|
client_hello: rustls::server::ClientHello<'_>,
|
||||||
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||||
let domain = client_hello.server_name()?;
|
let domain = client_hello.server_name()?;
|
||||||
|
|
||||||
match self.acme_manager.as_ref()?.get_certificate(domain) {
|
match (self.0).acme_manager.as_ref()?.get_certificate(domain) {
|
||||||
Err(acme::manager::GetCertificateError::NotFound) => {
|
Err(acme::manager::GetCertificateError::NotFound) => {
|
||||||
log::warn!("No cert found for domain {domain}");
|
log::warn!("No cert found for domain {domain}");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -360,3 +391,53 @@ impl rustls::server::ResolvesServerCert for ManagerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GeminiCertResolver(sync::Arc<ManagerImpl>);
|
||||||
|
|
||||||
|
impl From<sync::Arc<ManagerImpl>> for GeminiCertResolver {
|
||||||
|
fn from(mgr: sync::Arc<ManagerImpl>) -> Self {
|
||||||
|
Self(mgr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustls::server::ResolvesServerCert for GeminiCertResolver {
|
||||||
|
fn resolve(
|
||||||
|
&self,
|
||||||
|
client_hello: rustls::server::ClientHello<'_>,
|
||||||
|
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||||
|
let domain = client_hello.server_name()?;
|
||||||
|
|
||||||
|
let domain: domain::Name = match domain.parse() {
|
||||||
|
Ok(domain) => domain,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to parse domain name {domain}: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: unexpected::Result<Option<sync::Arc<rustls::sign::CertifiedKey>>> = (|| {
|
||||||
|
(self.0)
|
||||||
|
.gemini_store
|
||||||
|
.as_ref()
|
||||||
|
.or_unexpected_while("gemini store is not enabled")?
|
||||||
|
.get_certificate(&domain)
|
||||||
|
.or_unexpected_while("fetching pkey/cert")?
|
||||||
|
.map(|(pkey, cert)| {
|
||||||
|
let pkey = rustls::sign::any_supported_type(&pkey.into()).or_unexpected()?;
|
||||||
|
|
||||||
|
Ok(sync::Arc::new(rustls::sign::CertifiedKey {
|
||||||
|
cert: vec![cert.into()],
|
||||||
|
key: pkey,
|
||||||
|
ocsp: None,
|
||||||
|
sct_list: None,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
})();
|
||||||
|
|
||||||
|
res.unwrap_or_else(|err| {
|
||||||
|
log::error!("Unexpected error getting cert for domain {domain}: {err}");
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
33
src/main.rs
33
src/main.rs
@ -4,7 +4,7 @@ use clap::Parser;
|
|||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use signal_hook_tokio::Signals;
|
use signal_hook_tokio::Signals;
|
||||||
|
|
||||||
use std::{path, sync};
|
use std::path;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
@ -87,6 +87,8 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let gemini_enabled = config.service.gemini.gemini_addr.is_some();
|
||||||
|
|
||||||
let origin_store = domani::origin::git::FSStore::new(&config.origin)
|
let origin_store = domani::origin::git::FSStore::new(&config.origin)
|
||||||
.expect("git origin store initialization failed");
|
.expect("git origin store initialization failed");
|
||||||
|
|
||||||
@ -128,9 +130,14 @@ async fn main() {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let domain_gemini_store =
|
let domain_gemini_store = if gemini_enabled {
|
||||||
domani::domain::gemini::FSStore::new(&config.domain.store_dir_path.join("gemini"))
|
Some(
|
||||||
.unwrap_or_else(|e| panic!("domain gemini store initialization failed: {e}"));
|
domani::domain::gemini::FSStore::new(&config.domain.store_dir_path.join("gemini"))
|
||||||
|
.unwrap_or_else(|e| panic!("domain gemini store initialization failed: {e}")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut task_stack = domani::task_stack::TaskStack::new();
|
let mut task_stack = domani::task_stack::TaskStack::new();
|
||||||
|
|
||||||
@ -140,20 +147,24 @@ async fn main() {
|
|||||||
domain_store,
|
domain_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
domain_acme_manager,
|
domain_acme_manager,
|
||||||
|
domain_gemini_store,
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = domani::service::http::new(
|
let _ = domani::service::http::Service::new(
|
||||||
&mut task_stack,
|
&mut task_stack,
|
||||||
domain_manager.clone(),
|
domain_manager.clone(),
|
||||||
domain_manager.clone(),
|
domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()),
|
||||||
config.service.clone(),
|
config.service.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = domani::service::gemini::Service::new(
|
if gemini_enabled {
|
||||||
&mut task_stack,
|
let _ = domani::service::gemini::Service::new(
|
||||||
sync::Arc::new(domain_gemini_store),
|
&mut task_stack,
|
||||||
config.service,
|
domain_manager.clone(),
|
||||||
);
|
domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()),
|
||||||
|
config.service,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut signals =
|
let mut signals =
|
||||||
Signals::new(signal_hook::consts::TERM_SIGNALS).expect("initializing signals failed");
|
Signals::new(signal_hook::consts::TERM_SIGNALS).expect("initializing signals failed");
|
||||||
|
@ -332,7 +332,7 @@ impl super::Store for FSStore {
|
|||||||
|
|
||||||
// TODO this is very not ideal, the whole file is first read totally into memory, and then
|
// TODO this is very not ideal, the whole file is first read totally into memory, and then
|
||||||
// that is cloned.
|
// that is cloned.
|
||||||
let data = file_object.data.clone();
|
let data = bytes::Bytes::copy_from_slice(file_object.data.as_slice());
|
||||||
Ok(Box::pin(stream::once(async move { Ok(data) })))
|
Ok(Box::pin(stream::once(async move { Ok(data) })))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,3 +3,32 @@ pub mod gemini;
|
|||||||
pub mod http;
|
pub mod http;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
|
||||||
|
use std::borrow;
|
||||||
|
|
||||||
|
fn append_index_to_path<'path, 'index>(
|
||||||
|
path: &'path str,
|
||||||
|
index: &'index str,
|
||||||
|
) -> borrow::Cow<'path, str> {
|
||||||
|
if path.len() == 0 {
|
||||||
|
let mut path = String::with_capacity(1 + index.len());
|
||||||
|
path.push('/');
|
||||||
|
path.push_str(index);
|
||||||
|
return borrow::Cow::Owned(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.ends_with('/') {
|
||||||
|
let mut indexed_path = String::with_capacity(path.len() + index.len());
|
||||||
|
indexed_path.push_str(path.as_ref());
|
||||||
|
indexed_path.push_str(index);
|
||||||
|
return borrow::Cow::Owned(indexed_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
borrow::Cow::Borrowed(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guess_mime(path: &str) -> String {
|
||||||
|
mime_guess::from_path(path)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
@ -4,12 +4,13 @@ mod proxy;
|
|||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
|
||||||
use crate::error::unexpected::{self, Mappable};
|
use crate::error::unexpected::{self, Mappable};
|
||||||
use crate::{domain, service, task_stack};
|
use crate::{domain, service, task_stack, util};
|
||||||
|
|
||||||
use std::sync;
|
use std::sync;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
||||||
config: service::Config,
|
config: service::Config,
|
||||||
}
|
}
|
||||||
@ -24,19 +25,92 @@ enum HandleConnError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
pub fn new(
|
pub fn new<CertResolver>(
|
||||||
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
||||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
|
cert_resolver: CertResolver,
|
||||||
config: service::Config,
|
config: service::Config,
|
||||||
) -> sync::Arc<Service> {
|
) -> sync::Arc<Service>
|
||||||
|
where
|
||||||
|
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
||||||
|
{
|
||||||
let service = sync::Arc::new(Service {
|
let service = sync::Arc::new(Service {
|
||||||
cert_resolver,
|
domain_manager,
|
||||||
|
cert_resolver: sync::Arc::from(cert_resolver),
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
|
task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
|
||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn respond_conn<W>(
|
||||||
|
&self,
|
||||||
|
w: W,
|
||||||
|
code: &str,
|
||||||
|
meta: &str,
|
||||||
|
body: Option<util::BoxByteStream>,
|
||||||
|
) -> unexpected::Result<()>
|
||||||
|
where
|
||||||
|
W: tokio::io::AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::{copy, AsyncWriteExt, BufWriter};
|
||||||
|
let mut w = BufWriter::new(w);
|
||||||
|
|
||||||
|
w.write_all(code.as_bytes()).await.or_unexpected()?;
|
||||||
|
w.write_all(" ".as_bytes()).await.or_unexpected()?;
|
||||||
|
w.write_all(meta.as_bytes()).await.or_unexpected()?;
|
||||||
|
w.write_all("\r\n".as_bytes()).await.or_unexpected()?;
|
||||||
|
|
||||||
|
if let Some(body) = body {
|
||||||
|
let mut body = tokio_util::io::StreamReader::new(body);
|
||||||
|
copy(&mut body, &mut w).await.or_unexpected()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush().await.or_unexpected()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve_conn<IO>(&self, domain: &domain::Name, conn: IO) -> Result<(), HandleConnError>
|
||||||
|
where
|
||||||
|
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::*;
|
||||||
|
|
||||||
|
let (r, w) = split(conn);
|
||||||
|
let mut r = BufReader::new(r);
|
||||||
|
|
||||||
|
let mut req = String::with_capacity(64);
|
||||||
|
r.read_line(&mut req)
|
||||||
|
.await
|
||||||
|
.map_err(|e| HandleConnError::ClientError(format!("failed to read request: {e}")))?;
|
||||||
|
|
||||||
|
let req = gemini::request::parse::request(req.as_bytes())
|
||||||
|
.map(|(_, req)| req)
|
||||||
|
.map_err(|e| HandleConnError::ClientError(format!("failed to parse request: {e}")))?
|
||||||
|
.into_gemini_request()
|
||||||
|
.map_err(|e| HandleConnError::ClientError(format!("failed to parse request: {e}")))?;
|
||||||
|
|
||||||
|
let path = service::append_index_to_path(req.path(), "index.gmi");
|
||||||
|
|
||||||
|
let f = match self.domain_manager.get_file(domain, &path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(domain::manager::GetFileError::DomainNotFound) => {
|
||||||
|
return Err(unexpected::Error::from("domain not found when serving file").into())
|
||||||
|
}
|
||||||
|
Err(domain::manager::GetFileError::FileNotFound) => {
|
||||||
|
return Ok(self.respond_conn(w, "51", "File not found", None).await?)
|
||||||
|
}
|
||||||
|
Err(domain::manager::GetFileError::Unexpected(e)) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_type = service::guess_mime(&path);
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.respond_conn(w, "20", content_type.as_str(), Some(f))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn proxy_conn<IO>(
|
async fn proxy_conn<IO>(
|
||||||
&self,
|
&self,
|
||||||
proxied_domain: &ConfigProxiedDomain,
|
proxied_domain: &ConfigProxiedDomain,
|
||||||
@ -59,7 +133,7 @@ impl Service {
|
|||||||
async fn handle_conn(
|
async fn handle_conn(
|
||||||
&self,
|
&self,
|
||||||
conn: tokio::net::TcpStream,
|
conn: tokio::net::TcpStream,
|
||||||
_tls_config: sync::Arc<rustls::ServerConfig>,
|
tls_config: sync::Arc<rustls::ServerConfig>,
|
||||||
) -> Result<(), HandleConnError> {
|
) -> Result<(), HandleConnError> {
|
||||||
let teed_conn = {
|
let teed_conn = {
|
||||||
let (r, w) = tokio::io::split(conn);
|
let (r, w) = tokio::io::split(conn);
|
||||||
@ -92,9 +166,8 @@ impl Service {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(HandleConnError::ClientError(format!(
|
let conn = start.into_stream(tls_config).await.or_unexpected()?;
|
||||||
"unknown domain {domain}"
|
self.serve_conn(&domain, conn).await
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(unexpected::Error::from(
|
return Err(unexpected::Error::from(
|
||||||
@ -110,20 +183,26 @@ async fn listen(
|
|||||||
service: sync::Arc<Service>,
|
service: sync::Arc<Service>,
|
||||||
canceller: CancellationToken,
|
canceller: CancellationToken,
|
||||||
) -> unexpected::Result<()> {
|
) -> unexpected::Result<()> {
|
||||||
|
let addr = &service
|
||||||
|
.config
|
||||||
|
.gemini
|
||||||
|
.gemini_addr
|
||||||
|
.expect("listen called with gemini_addr not set");
|
||||||
|
|
||||||
let tls_config = sync::Arc::new(
|
let tls_config = sync::Arc::new(
|
||||||
rustls::server::ServerConfig::builder()
|
rustls::server::ServerConfig::builder()
|
||||||
.with_safe_defaults()
|
.with_safe_defaults()
|
||||||
.with_no_client_auth() // TODO maybe this isn't right?
|
.with_no_client_auth()
|
||||||
.with_cert_resolver(service.cert_resolver.clone()),
|
.with_cert_resolver(service.cert_resolver.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Listening on gemini://{}:{}",
|
"Listening on gemini://{}:{}",
|
||||||
&service.config.primary_domain.clone(),
|
&service.config.primary_domain.clone(),
|
||||||
&service.config.gemini.gemini_addr.port(),
|
addr,
|
||||||
);
|
);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(service.config.gemini.gemini_addr)
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
.await
|
.await
|
||||||
.or_unexpected_while("binding tcp socket")?;
|
.or_unexpected_while("binding tcp socket")?;
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ use serde_with::{serde_as, TryFromInto};
|
|||||||
|
|
||||||
use std::{collections, net, str::FromStr};
|
use std::{collections, net, str::FromStr};
|
||||||
|
|
||||||
fn default_gemini_addr() -> net::SocketAddr {
|
fn default_gemini_addr() -> Option<net::SocketAddr> {
|
||||||
net::SocketAddr::from_str("[::]:3965").unwrap()
|
Some(net::SocketAddr::from_str("[::]:3965").unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
@ -65,7 +65,7 @@ pub struct ConfigProxiedDomain {
|
|||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_gemini_addr")]
|
#[serde(default = "default_gemini_addr")]
|
||||||
pub gemini_addr: net::SocketAddr,
|
pub gemini_addr: Option<net::SocketAddr>,
|
||||||
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
|
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,31 +23,6 @@ pub struct Service {
|
|||||||
config: service::Config,
|
config: service::Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
|
||||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
|
||||||
config: service::Config,
|
|
||||||
) -> sync::Arc<Service> {
|
|
||||||
let https_enabled = config.http.https_addr.is_some();
|
|
||||||
|
|
||||||
let service = sync::Arc::new(Service {
|
|
||||||
domain_manager: domain_manager.clone(),
|
|
||||||
cert_resolver,
|
|
||||||
handlebars: tpl::get(),
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
|
|
||||||
|
|
||||||
if https_enabled {
|
|
||||||
task_stack.push_spawn(|canceller| tasks::listen_https(service.clone(), canceller));
|
|
||||||
task_stack.push_spawn(|canceller| tasks::cert_refresher(service.clone(), canceller));
|
|
||||||
}
|
|
||||||
|
|
||||||
service
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BasePresenter<'a, T> {
|
struct BasePresenter<'a, T> {
|
||||||
page_name: &'a str,
|
page_name: &'a str,
|
||||||
@ -77,15 +52,39 @@ struct DomainSyncArgs {
|
|||||||
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'svc> Service {
|
impl Service {
|
||||||
fn serve(&self, status_code: u16, path: &str, body: Body) -> Response<Body> {
|
pub fn new<CertResolver>(
|
||||||
let content_type = mime_guess::from_path(path)
|
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
||||||
.first_or_octet_stream()
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
.to_string();
|
cert_resolver: CertResolver,
|
||||||
|
config: service::Config,
|
||||||
|
) -> sync::Arc<Service>
|
||||||
|
where
|
||||||
|
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
||||||
|
{
|
||||||
|
let https_enabled = config.http.https_addr.is_some();
|
||||||
|
|
||||||
|
let service = sync::Arc::new(Service {
|
||||||
|
domain_manager: domain_manager.clone(),
|
||||||
|
cert_resolver: sync::Arc::from(cert_resolver),
|
||||||
|
handlebars: tpl::get(),
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
|
||||||
|
|
||||||
|
if https_enabled {
|
||||||
|
task_stack.push_spawn(|canceller| tasks::listen_https(service.clone(), canceller));
|
||||||
|
task_stack.push_spawn(|canceller| tasks::cert_refresher(service.clone(), canceller));
|
||||||
|
}
|
||||||
|
|
||||||
|
service
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve(&self, status_code: u16, path: &str, body: Body) -> Response<Body> {
|
||||||
match Response::builder()
|
match Response::builder()
|
||||||
.status(status_code)
|
.status(status_code)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", service::guess_mime(path))
|
||||||
.body(body)
|
.body(body)
|
||||||
{
|
{
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
@ -161,20 +160,10 @@ impl<'svc> Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_origin(&self, domain: domain::Name, req: Request<Body>) -> Response<Body> {
|
async fn serve_origin(&self, domain: domain::Name, req: Request<Body>) -> Response<Body> {
|
||||||
let mut path_owned;
|
let path = service::append_index_to_path(req.uri().path(), "index.html");
|
||||||
let path = req.uri().path();
|
|
||||||
|
|
||||||
let path = match path.ends_with('/') {
|
match self.domain_manager.get_file(&domain, &path) {
|
||||||
true => {
|
Ok(f) => self.serve(200, &path, Body::wrap_stream(f)),
|
||||||
path_owned = String::from(path);
|
|
||||||
path_owned.push_str("index.html");
|
|
||||||
path_owned.as_str()
|
|
||||||
}
|
|
||||||
false => path,
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.domain_manager.get_file(&domain, path) {
|
|
||||||
Ok(f) => self.serve(200, path, Body::wrap_stream(f)),
|
|
||||||
Err(domain::manager::GetFileError::DomainNotFound) => {
|
Err(domain::manager::GetFileError::DomainNotFound) => {
|
||||||
return self.render_error_page(404, "Domain not found")
|
return self.render_error_page(404, "Domain not found")
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,6 @@ pub fn open_file(path: &path::Path) -> io::Result<Option<fs::File>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type BoxByteStream = futures::stream::BoxStream<'static, io::Result<Vec<u8>>>;
|
pub type BoxByteStream = futures::stream::BoxStream<'static, io::Result<bytes::Bytes>>;
|
||||||
|
|
||||||
pub type BoxFuture<'a, O> = pin::Pin<Box<dyn futures::Future<Output = O> + Send + 'a>>;
|
pub type BoxFuture<'a, O> = pin::Pin<Box<dyn futures::Future<Output = O> + Send + 'a>>;
|
||||||
|
Loading…
Reference in New Issue
Block a user