summaryrefslogtreecommitdiff
path: root/crates/chat-server
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 22:36:11 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 22:36:11 -0500
commit6933ec28be3dac0dc17cd634c663db3b4818caad (patch)
tree22690260136d598d2719d97deb169b847b31424c /crates/chat-server
parentc3b361c45ac8dd898d5c83a161abed5fc138b6c0 (diff)
Centralize deployment settings in the root environment file
Document public domains, ports, upstreams, chat binding, and backup defaults alongside private settings. Validate shared configuration and render Caddy, cgit, and website links from an explicit public allowlist while preserving one installation environment file.
Diffstat (limited to 'crates/chat-server')
-rw-r--r--crates/chat-server/Cargo.toml2
-rw-r--r--crates/chat-server/src/http.rs20
-rw-r--r--crates/chat-server/src/main.rs34
-rw-r--r--crates/chat-server/web/index.html4
4 files changed, 35 insertions, 25 deletions
diff --git a/crates/chat-server/Cargo.toml b/crates/chat-server/Cargo.toml
index eecebf6..709c37f 100644
--- a/crates/chat-server/Cargo.toml
+++ b/crates/chat-server/Cargo.toml
@@ -27,11 +27,11 @@ categories = ["web-programming"]
publish = false
[dependencies]
+stack-config = { path = "../config", version = "0.1.0" }
anyhow = "1"
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
base64 = "0.22"
clap = { version = "4.6", features = ["derive"] }
-dotenvy = "0.15"
hex = "0.4"
openssl = "0.10"
rusqlite = { version = "0.41", default-features = false, features = ["bundled", "backup"] }
diff --git a/crates/chat-server/src/http.rs b/crates/chat-server/src/http.rs
index bb618d9..1c7286f 100644
--- a/crates/chat-server/src/http.rs
+++ b/crates/chat-server/src/http.rs
@@ -33,6 +33,7 @@ struct App {
permits: Arc<Semaphore>,
root: PathBuf,
origin: String,
+ public: std::collections::BTreeMap<String, String>,
}
fn secured(mut response: Response) -> Response {
@@ -129,22 +130,33 @@ async fn asset(State(app): State<App>, uri: axum::http::Uri) -> Response {
"/LICENSE" => ("LICENSE", "text/plain; charset=utf-8"),
_ => return secured(StatusCode::NOT_FOUND.into_response()),
};
- tokio::fs::read(app.root.join(path)).await.map_or_else(
- |_| secured(StatusCode::NOT_FOUND.into_response()),
- |bytes| secured(([("content-type", mime)], bytes).into_response()),
- )
+ let Ok(bytes) = tokio::fs::read(app.root.join(path)).await else {
+ return secured(StatusCode::NOT_FOUND.into_response());
+ };
+ if matches!(uri.path(), "/" | "/javascript.html") {
+ let rendered = String::from_utf8(bytes)
+ .map_err(anyhow::Error::from)
+ .and_then(|source| stack_config::render(&source, &app.public));
+ return rendered.map_or_else(
+ |_| secured(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
+ |html| secured(([("content-type", mime)], html).into_response()),
+ );
+ }
+ secured(([("content-type", mime)], bytes).into_response())
}
pub(crate) async fn serve(
listener: tokio::net::TcpListener,
service: Service,
root: &Path,
+ public: std::collections::BTreeMap<String, String>,
) -> Result<()> {
let app = App {
origin: service.origin.clone(),
service: Arc::new(Mutex::new(service)),
permits: Arc::new(Semaphore::new(4)),
root: root.to_path_buf(),
+ public,
};
let routes = Router::new()
.route("/api", post(api))
diff --git a/crates/chat-server/src/main.rs b/crates/chat-server/src/main.rs
index c17a7f6..cfd3e7a 100644
--- a/crates/chat-server/src/main.rs
+++ b/crates/chat-server/src/main.rs
@@ -32,8 +32,8 @@ use std::process::ExitCode;
struct Arguments {
#[arg(long, default_value = ".")]
root: PathBuf,
- #[arg(long, default_value = "127.0.0.1:9080")]
- bind: SocketAddr,
+ #[arg(long)]
+ bind: Option<SocketAddr>,
#[arg(long, conflicts_with_all = ["reset_owner", "owner_fingerprint", "snapshot"])]
init: bool,
#[arg(long, value_name = "FINGERPRINT", conflicts_with_all = ["owner_fingerprint", "snapshot"], help = "Permanently clear the matching owner registration and all chat history after key loss")]
@@ -73,7 +73,7 @@ fn initialize(root: &std::path::Path) -> Result<()> {
{
writeln!(file, "CHAT_ORIGIN=http://127.0.0.1:9080")?;
}
- Ok(())
+ stack_config::initialize_public(root)
}
async fn run(arguments: Arguments) -> Result<()> {
@@ -81,6 +81,8 @@ async fn run(arguments: Arguments) -> Result<()> {
if arguments.init {
return initialize(&root);
}
+ let configuration = stack_config::Config::load(&root)?;
+ let bind = arguments.bind.unwrap_or(configuration.chat_bind()?);
let data = root.join("var/data/chat");
std::fs::create_dir_all(&data)?;
std::fs::set_permissions(&data, std::os::unix::fs::PermissionsExt::from_mode(0o700))?;
@@ -99,7 +101,7 @@ async fn run(arguments: Arguments) -> Result<()> {
return maintenance::fingerprint(&store);
}
if let Some(expected) = arguments.reset_owner {
- return maintenance::reset(&mut store, &expected, arguments.bind);
+ return maintenance::reset(&mut store, &expected, bind);
}
if let Some(destination) = arguments.snapshot {
store.snapshot(&destination)?;
@@ -110,28 +112,24 @@ async fn run(arguments: Arguments) -> Result<()> {
return Ok(());
}
ensure!(
- arguments.bind.ip().is_loopback(),
+ bind.ip().is_loopback(),
"Experimental server must bind to loopback"
);
- let settings = dotenvy::from_path_iter(root.join(".env"))?
- .collect::<std::result::Result<std::collections::HashMap<_, _>, _>>()?;
- let origin = settings
- .get("CHAT_ORIGIN")
- .context("Run --init to configure chat")?;
+ let origin = configuration.value("CHAT_ORIGIN")?;
ensure!(
- origin == &format!("http://{}", arguments.bind),
- "This development server requires CHAT_ORIGIN to match its loopback bind address"
+ origin == format!("http://{bind}"),
+ "CHAT_ORIGIN must match the loopback CHAT_BIND or --bind address"
);
- let setup = settings
- .get("CHAT_SETUP_TOKEN")
- .context("Missing CHAT_SETUP_TOKEN")?;
- let state = service::Service::new(store, origin.clone(), setup.clone());
- let listener = tokio::net::TcpListener::bind(arguments.bind).await?;
+ let setup = configuration
+ .value("CHAT_SETUP_TOKEN")
+ .context("Run --init to configure chat")?;
+ let state = service::Service::new(store, origin.to_owned(), setup.to_owned());
+ let listener = tokio::net::TcpListener::bind(bind).await?;
writeln!(
std::io::stdout().lock(),
"Experimental chat: {origin} (owner setup: /#setup)"
)?;
- http::serve(listener, state, &root).await
+ http::serve(listener, state, &root, configuration.public_values(None)?).await
}
#[tokio::main]
diff --git a/crates/chat-server/web/index.html b/crates/chat-server/web/index.html
index 7c2fe8b..79026b0 100644
--- a/crates/chat-server/web/index.html
+++ b/crates/chat-server/web/index.html
@@ -29,14 +29,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="libre-badge">[ <a href="https://www.gnu.org/software/librejs/" aria-describedby="js-warning">GNU LibreJS-compliant</a><span id="js-warning" role="tooltip">LibreJS compliance concerns software freedom, not a guarantee of privacy or security. Review the source code, dependencies, and scripts delivered to your browser. Server-delivered code can change between visits; free software can still track users or behave maliciously.</span> ]
<a class="script-licenses" href="/javascript.html" rel="jslicense">[ JAVASCRIPT LICENSES ]</a></div>
<header>
-<pre class="logo-art"><a href="https://doan.luuduct.ai/" class="logo" aria-label="Doan Luu Duc Tai home">
+<pre class="logo-art"><a href="{{SITE_ORIGIN}}/" class="logo" aria-label="Doan Luu Duc Tai home">
____ ___ _ _ _
| _ \ / _ \ / \ | \ | |
| | | | | | |/ _ \ | \| |
| |_| | |_| / ___ \| |\ |
|____/ \___/_/ \_\_| \_| LUU DUC TAI
</a></pre>
-<nav class="site-nav" aria-label="Site navigation"><a href="https://doan.luuduct.ai/">Home</a><a href="https://doan.luuduct.ai/#services">Services</a><a href="/" aria-current="page">Message Me</a></nav>
+<nav class="site-nav" aria-label="Site navigation"><a href="{{SITE_ORIGIN}}/">Home</a><a href="{{SITE_ORIGIN}}/#services">Services</a><a href="/" aria-current="page">Message Me</a></nav>
</header>
<div class="ascii-panel introduction">
<div class="ascii-bar" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div>