fix: CSRF race condition, UI containers, Tor ordering, seed layout
- session.rs: use OnceCell for remember_secret to prevent concurrent requests on first boot from generating different HMAC secrets, which caused CSRF token mismatch on every state-changing RPC call (app install, start, stop all failed with "CSRF token missing or invalid") - install.rs: write lnd.conf with Bitcoin RPC credentials before LND container starts (prevents "bitcoin.mainnet must be specified" crash); inject Bitcoin RPC auth into bitcoin-ui nginx.conf; add proper error logging to UI container build/run steps; fix UI containers to use --network=host (they proxy to localhost backend/bitcoin RPC) - Tor: remove After=tor.service from archipelago-tor-helper.path to break systemd ordering cycle that prevented Tor from starting on boot - Seed screen: compact grid layout (2 cols mobile, 4 cols sm+) with tighter padding to fit kiosk displays without scrolling - Dockerfiles: remove nonexistent assets/ COPY from bitcoin-ui, fix electrs-ui to COPY qrcode.js and EXPOSE 50002 (matches nginx.conf) - image-versions.sh: add UI container image variables for registry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
030015fce6
commit
4b0e1cfbe3
@ -13,6 +13,24 @@ use anyhow::{Context, Result};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
const INSTALL_LOG: &str = "/var/log/archipelago-container-installs.log";
|
||||
|
||||
/// Append a timestamped line to the persistent install log.
|
||||
async fn install_log(msg: &str) {
|
||||
let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
|
||||
let line = format!("[{}] {}\n", ts, msg);
|
||||
let _ = tokio::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(INSTALL_LOG)
|
||||
.await
|
||||
.and_then(|mut f| {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
Box::pin(async move { f.write_all(line.as_bytes()).await })
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
/// Install a package from a Docker image.
|
||||
/// Security: Image verification, resource limits, network isolation.
|
||||
@ -33,12 +51,14 @@ impl RpcHandler {
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing dockerImage"))?;
|
||||
|
||||
install_log(&format!("INSTALL START: {} (image: {})", package_id, docker_image)).await;
|
||||
debug!(
|
||||
"Installing package {} from image {}",
|
||||
package_id, docker_image
|
||||
);
|
||||
|
||||
if !is_valid_docker_image(docker_image) {
|
||||
install_log(&format!("INSTALL FAIL: {} — invalid image format", package_id)).await;
|
||||
return Err(anyhow::anyhow!("Invalid Docker image format"));
|
||||
}
|
||||
|
||||
@ -80,9 +100,11 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
// Pull or verify image
|
||||
install_log(&format!("INSTALL PULL: {} — pulling image {}", package_id, docker_image)).await;
|
||||
let has_local_fallback = self
|
||||
.pull_or_verify_image(package_id, docker_image)
|
||||
.await?;
|
||||
install_log(&format!("INSTALL PULL OK: {} — image ready (local_fallback={})", package_id, has_local_fallback)).await;
|
||||
|
||||
// Normalize container name for legacy aliases
|
||||
let container_name = match package_id {
|
||||
@ -173,6 +195,11 @@ impl RpcHandler {
|
||||
self.write_bitcoin_conf(&rpc_user, &rpc_pass).await;
|
||||
}
|
||||
|
||||
// Pre-install: lnd.conf with Bitcoin RPC credentials
|
||||
if package_id == "lnd" {
|
||||
self.write_lnd_conf(&rpc_user, &rpc_pass).await;
|
||||
}
|
||||
|
||||
// Pre-install: SearXNG settings.yml (required or container exits immediately)
|
||||
if package_id == "searxng" {
|
||||
let searx_dir = "/var/lib/archipelago/searxng";
|
||||
@ -244,6 +271,7 @@ impl RpcHandler {
|
||||
|
||||
if !run_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&run_output.stderr);
|
||||
install_log(&format!("INSTALL FAIL: {} — podman run failed: {}", package_id, stderr)).await;
|
||||
// Rollback: remove partially created container
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["rm", "-f", container_name])
|
||||
@ -305,6 +333,8 @@ impl RpcHandler {
|
||||
// Post-install hooks — await completion before returning success
|
||||
self.run_post_install_hooks(package_id).await;
|
||||
|
||||
install_log(&format!("INSTALL OK: {} (container: {})", package_id, &container_id[..12.min(container_id.len())])).await;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": package_id,
|
||||
@ -544,6 +574,47 @@ printtoconsole=1\n",
|
||||
info!("Created bitcoin.conf with rpcauth (no plaintext credentials)");
|
||||
}
|
||||
|
||||
/// Write LND config file with Bitcoin RPC credentials.
|
||||
async fn write_lnd_conf(&self, rpc_user: &str, rpc_pass: &str) {
|
||||
let lnd_dir = "/var/lib/archipelago/lnd";
|
||||
let conf_path = format!("{}/lnd.conf", lnd_dir);
|
||||
|
||||
// Don't overwrite existing config (user may have customized it)
|
||||
if tokio::fs::try_exists(&conf_path).await.unwrap_or(false) {
|
||||
info!("lnd.conf already exists, skipping write");
|
||||
return;
|
||||
}
|
||||
|
||||
let lnd_conf = format!(
|
||||
"\
|
||||
[Application Options]\n\
|
||||
listen=0.0.0.0:9735\n\
|
||||
rpclisten=0.0.0.0:10009\n\
|
||||
restlisten=0.0.0.0:8080\n\
|
||||
debuglevel=info\n\
|
||||
noseedbackup=true\n\
|
||||
\n\
|
||||
[Bitcoin]\n\
|
||||
bitcoin.mainnet=true\n\
|
||||
bitcoin.node=bitcoind\n\
|
||||
\n\
|
||||
[Bitcoind]\n\
|
||||
bitcoind.rpchost=bitcoin-knots:8332\n\
|
||||
bitcoind.rpcuser={user}\n\
|
||||
bitcoind.rpcpass={pass}\n\
|
||||
bitcoind.rpcpolling=true\n\
|
||||
bitcoind.estimatemode=ECONOMICAL\n\
|
||||
\n\
|
||||
[autopilot]\n\
|
||||
autopilot.active=false\n",
|
||||
user = rpc_user,
|
||||
pass = rpc_pass,
|
||||
);
|
||||
let _ = tokio::fs::create_dir_all(lnd_dir).await;
|
||||
let _ = tokio::fs::write(&conf_path, lnd_conf).await;
|
||||
info!("Created lnd.conf with Bitcoin RPC credentials");
|
||||
}
|
||||
|
||||
/// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container).
|
||||
/// Critical hooks (credential setup, config) are awaited; UI container builds are background.
|
||||
async fn run_post_install_hooks(&self, package_id: &str) {
|
||||
@ -647,54 +718,105 @@ printtoconsole=1\n",
|
||||
info!("Nextcloud trusted domains configured for {}", host_ip);
|
||||
}
|
||||
|
||||
// Build and start companion UI containers for headless services
|
||||
let ui_builds: Vec<(&str, &str, &str, &str)> = match package_id {
|
||||
// Pre-build: inject Bitcoin RPC auth into bitcoin-ui nginx.conf
|
||||
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
use base64::Engine;
|
||||
let auth_b64 = base64::engine::general_purpose::STANDARD
|
||||
.encode(format!("{}:{}", rpc_user, rpc_pass));
|
||||
for dir in ["/opt/archipelago/docker/bitcoin-ui", "/home/archipelago/archy/docker/bitcoin-ui"] {
|
||||
let conf_path = format!("{}/nginx.conf", dir);
|
||||
if let Ok(content) = tokio::fs::read_to_string(&conf_path).await {
|
||||
if content.contains("__BITCOIN_RPC_AUTH__") {
|
||||
let updated = content.replace("__BITCOIN_RPC_AUTH__", &auth_b64);
|
||||
let _ = tokio::fs::write(&conf_path, updated).await;
|
||||
info!("Injected Bitcoin RPC auth into {}", conf_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build and start companion UI containers for headless services.
|
||||
// All UIs proxy to localhost (backend :5678 or bitcoin :8332) so they need --network=host.
|
||||
let ui_builds: Vec<(&str, &str, &str)> = match package_id {
|
||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => {
|
||||
vec![("bitcoin-ui", "/opt/archipelago/docker/bitcoin-ui", "localhost/bitcoin-ui", "8334:80")]
|
||||
vec![("archy-bitcoin-ui", "/opt/archipelago/docker/bitcoin-ui", "bitcoin-ui")]
|
||||
}
|
||||
"lnd" => {
|
||||
vec![("archy-lnd-ui", "/opt/archipelago/docker/lnd-ui", "localhost/lnd-ui", "8081:80")]
|
||||
vec![("archy-lnd-ui", "/opt/archipelago/docker/lnd-ui", "lnd-ui")]
|
||||
}
|
||||
"electrumx" | "electrs" | "mempool-electrs" => {
|
||||
vec![("archy-electrs-ui", "/opt/archipelago/docker/electrs-ui", "localhost/electrs-ui", "50002:80")]
|
||||
vec![("archy-electrs-ui", "/opt/archipelago/docker/electrs-ui", "electrs-ui")]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for (name, ui_dir, image, port) in ui_builds {
|
||||
for (name, ui_dir, image_base) in ui_builds {
|
||||
let name = name.to_string();
|
||||
let ui_dir = ui_dir.to_string();
|
||||
let image = image.to_string();
|
||||
let port = port.to_string();
|
||||
let image_base = image_base.to_string();
|
||||
let registry = "80.71.235.15:3000/archipelago";
|
||||
let registry_image = format!("{}/{}:latest", registry, image_base);
|
||||
let local_image = format!("localhost/{}:latest", image_base);
|
||||
tokio::spawn(async move {
|
||||
if !std::path::Path::new(&ui_dir).exists() {
|
||||
info!("UI source not found at {}, skipping", ui_dir);
|
||||
return;
|
||||
}
|
||||
info!("Building UI container {} from {}", name, ui_dir);
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["build", "-t", &image, &ui_dir])
|
||||
.output()
|
||||
.await;
|
||||
// Remove existing container
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["rm", "-f", &name])
|
||||
.output()
|
||||
.await;
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
|
||||
// Try registry image first, fall back to local build
|
||||
let image = {
|
||||
let pull = tokio::process::Command::new("podman")
|
||||
.args(["pull", ®istry_image])
|
||||
.output()
|
||||
.await;
|
||||
if pull.map_or(false, |o| o.status.success()) {
|
||||
info!("Pulled {} UI from registry", name);
|
||||
registry_image.clone()
|
||||
} else if std::path::Path::new(&ui_dir).exists() {
|
||||
info!("Registry pull failed, building {} from {}", name, ui_dir);
|
||||
let build = tokio::process::Command::new("podman")
|
||||
.args(["build", "-t", &local_image, &ui_dir])
|
||||
.output()
|
||||
.await;
|
||||
match build {
|
||||
Ok(o) if o.status.success() => local_image,
|
||||
Ok(o) => {
|
||||
warn!("Failed to build {}: {}", name,
|
||||
String::from_utf8_lossy(&o.stderr));
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to build {}: {}", name, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("No registry image or source for {} — skipping", name);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Run with --network=host (UIs proxy to localhost backend/bitcoin)
|
||||
let run = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"run", "-d",
|
||||
"--name", &name,
|
||||
"--restart=unless-stopped",
|
||||
"--network=archy-net",
|
||||
"--network=host",
|
||||
"--cap-drop=ALL",
|
||||
"--cap-add=NET_BIND_SERVICE",
|
||||
"--memory=64m",
|
||||
"-p", &port,
|
||||
&format!("{}:latest", image),
|
||||
&image,
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
info!("{} UI container started on port {}", name, port);
|
||||
match run {
|
||||
Ok(o) if o.status.success() => info!("{} UI container started (host network)", name),
|
||||
Ok(o) => warn!("Failed to start {}: {}", name, String::from_utf8_lossy(&o.stderr)),
|
||||
Err(e) => warn!("Failed to start {}: {}", name, e),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,12 @@ impl DockerPackageScanner {
|
||||
"immich_redis",
|
||||
"endurain-db",
|
||||
"nextcloud-db",
|
||||
"indeedhub-api",
|
||||
"indeedhub-ffmpeg",
|
||||
"indeedhub-postgres",
|
||||
"indeedhub-redis",
|
||||
"indeedhub-minio",
|
||||
"indeedhub-relay",
|
||||
"indeedhub-build_api_1",
|
||||
"indeedhub-build_postgres_1",
|
||||
"indeedhub-build_redis_1",
|
||||
|
||||
@ -5,9 +5,12 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Cached remember secret — loaded once, never regenerated within a process.
|
||||
static REMEMBER_SECRET: OnceCell<Vec<u8>> = OnceCell::const_new();
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
const FULL_SESSION_TTL: u64 = 86400; // 24 hours of inactivity
|
||||
@ -391,21 +394,23 @@ impl SessionStore {
|
||||
}
|
||||
|
||||
pub async fn load_or_create_remember_secret() -> Vec<u8> {
|
||||
// Try existing secret file first
|
||||
if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await {
|
||||
if secret.len() == 32 {
|
||||
return secret;
|
||||
REMEMBER_SECRET.get_or_init(|| async {
|
||||
// Try existing secret file first
|
||||
if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await {
|
||||
if secret.len() == 32 {
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Generate a cryptographically random 32-byte secret on first boot
|
||||
let mut secret = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut secret);
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await;
|
||||
secret.to_vec()
|
||||
// Generate a cryptographically random 32-byte secret on first boot
|
||||
let mut secret = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut secret);
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await;
|
||||
secret.to_vec()
|
||||
}).await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
FROM 80.71.235.15:3000/archipelago/nginx:1.27.4-alpine
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY 50x.html /usr/share/nginx/html/
|
||||
COPY assets/ /usr/share/nginx/html/assets/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 8334
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
FROM 80.71.235.15:3000/archipelago/nginx:1.27.4-alpine
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY 50x.html /usr/share/nginx/html/
|
||||
COPY assets/ /usr/share/nginx/html/assets/
|
||||
COPY qrcode.js /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
EXPOSE 50002
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
[Unit]
|
||||
Description=Watch for Archipelago Tor management actions
|
||||
After=tor.service
|
||||
|
||||
[Path]
|
||||
PathExists=/var/lib/archipelago/tor-config/tor-action
|
||||
|
||||
@ -36,14 +36,14 @@
|
||||
|
||||
<!-- Word Grid -->
|
||||
<div v-if="words.length > 0" class="w-full max-w-[600px]">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 gap-1.5 sm:gap-2">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-1 sm:gap-1.5">
|
||||
<div
|
||||
v-for="(word, i) in words"
|
||||
:key="i"
|
||||
class="bg-black/60 rounded-lg px-3 py-1.5 sm:py-2 border border-white/10"
|
||||
class="bg-black/60 rounded-lg px-2.5 py-1 sm:py-1.5 border border-white/10"
|
||||
>
|
||||
<span class="text-white/40 text-[1rem] font-mono mr-1.5">{{ i + 1 }}.</span>
|
||||
<span class="text-white/95 text-[1.2rem] font-mono">{{ word }}</span>
|
||||
<span class="text-white/40 text-sm font-mono mr-1">{{ i + 1 }}.</span>
|
||||
<span class="text-white/95 text-[1.05rem] font-mono">{{ word }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ export const SERVICE_NAMES = new Set([
|
||||
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
||||
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
||||
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
||||
'indeedhub-api', 'indeedhub-ffmpeg',
|
||||
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
||||
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
||||
'indeedhub-build_minio-init_1', 'indeedhub-build_relay_1',
|
||||
|
||||
@ -81,5 +81,10 @@ PENPOT_BACKEND_IMAGE="$ARCHY_REGISTRY/penpot-backend:2.4"
|
||||
PENPOT_EXPORTER_IMAGE="$ARCHY_REGISTRY/penpot-exporter:2.4"
|
||||
PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4"
|
||||
|
||||
# Custom UI containers (built from docker/ dirs, pushed to registry)
|
||||
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:latest"
|
||||
LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:latest"
|
||||
ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest"
|
||||
|
||||
# Base images
|
||||
NGINX_ALPINE_IMAGE="$ARCHY_REGISTRY/nginx:1.27.4-alpine"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user