feat: bitcoin-ui CSS fix, HTTPS proxy support, deploy script improvements
Bitcoin UI: - Replace cdn.tailwindcss.com with locally bundled tailwind.css (CSP blocks external scripts) - Make all asset paths relative for nginx proxy compatibility - Add bitcoin-ui build/deploy to deploy-to-target.sh (was missing entirely) - Use --network host (bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332) HTTPS mixed content fix: - Add HTTPS_PROXY_PATHS in AppSession.vue — when parent page is HTTPS, iframe loads through nginx proxy instead of direct HTTP port - Prevents browser blocking HTTP iframes inside HTTPS pages - All Tailscale servers use HTTPS, this was breaking all app iframes Deploy & first-boot improvements: - first-boot-containers.sh auto-detects disk size for pruning vs txindex - first-boot-containers.sh checks fallback source path for UI containers - Added mempool-electrs to APP_PORTS mapping - ElectrumX container creation in first-boot - Podman doctor/fix/uptime skills added Also includes: session persistence, identity management, LND transactions, ElectrumX status UI, nostr-provider improvements, Web5 enhancements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4e54b8bd4d
commit
367b483a72
@@ -35,7 +35,7 @@ use crate::config::Config;
|
||||
use crate::container::DevContainerOrchestrator;
|
||||
use crate::monitoring::MetricsStore;
|
||||
use crate::port_allocator::PortAllocator;
|
||||
use crate::session::{self, EndpointRateLimiter, LoginRateLimiter, SessionStore};
|
||||
use crate::session::{self, EndpointRateLimiter, LoginRateLimiter, SessionStore, REMEMBER_TTL};
|
||||
use crate::state::StateManager;
|
||||
use anyhow::{Context, Result};
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
@@ -221,12 +221,30 @@ impl RpcHandler {
|
||||
|
||||
// Enforce authentication for non-allowlisted methods
|
||||
let is_unauthenticated = UNAUTHENTICATED_METHODS.contains(&rpc_req.method.as_str());
|
||||
let mut new_session_cookies: Option<(String, String)> = None; // (session, csrf) if auto-restored
|
||||
if !is_unauthenticated {
|
||||
let authenticated = match &session_token {
|
||||
let mut authenticated = match &session_token {
|
||||
Some(token) => self.session_store.validate(token).await,
|
||||
None => false,
|
||||
};
|
||||
|
||||
// If session invalid, try remember-me token to auto-restore session
|
||||
if !authenticated {
|
||||
if let Some(remember) = extract_cookie(&parts.headers, "remember") {
|
||||
if crate::session::SessionStore::validate_remember_token(&remember) {
|
||||
// Auto-create a new session from the remember-me token
|
||||
let new_token = self.session_store.create().await;
|
||||
let new_csrf = generate_csrf_token();
|
||||
tracing::info!("Auto-restored session from remember-me token");
|
||||
new_session_cookies = Some((new_token, new_csrf));
|
||||
authenticated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
let reason = if session_token.is_none() { "no session cookie" } else { "invalid/expired token" };
|
||||
tracing::warn!(method = %rpc_req.method, reason, "401 Unauthorized — rejecting RPC call");
|
||||
let rpc_resp = RpcResponse {
|
||||
result: None,
|
||||
error: Some(RpcError {
|
||||
@@ -269,7 +287,8 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
// CSRF protection: validate X-CSRF-Token header for authenticated methods
|
||||
if !is_unauthenticated {
|
||||
// Skip CSRF check if session was just auto-restored from remember-me (new CSRF will be set in response)
|
||||
if !is_unauthenticated && new_session_cookies.is_none() {
|
||||
let csrf_cookie = extract_csrf_cookie(&parts.headers);
|
||||
let csrf_header = parts
|
||||
.headers
|
||||
@@ -280,6 +299,12 @@ impl RpcHandler {
|
||||
match (&csrf_cookie, &csrf_header) {
|
||||
(Some(cookie), Some(header)) if cookie == header => { /* valid */ }
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
method = %rpc_req.method,
|
||||
has_cookie = csrf_cookie.is_some(),
|
||||
has_header = csrf_header.is_some(),
|
||||
"403 CSRF mismatch — rejecting RPC call"
|
||||
);
|
||||
let rpc_resp = RpcResponse {
|
||||
result: None,
|
||||
error: Some(RpcError {
|
||||
@@ -445,6 +470,7 @@ impl RpcHandler {
|
||||
"lnd.payinvoice" => self.handle_lnd_payinvoice(params).await,
|
||||
"lnd.create-psbt" => self.handle_lnd_create_psbt(params).await,
|
||||
"lnd.finalize-psbt" => self.handle_lnd_finalize_psbt(params).await,
|
||||
"lnd.gettransactions" => self.handle_lnd_gettransactions().await,
|
||||
|
||||
// Multi-identity management
|
||||
"identity.list" => self.handle_identity_list(params).await,
|
||||
@@ -461,6 +487,9 @@ impl RpcHandler {
|
||||
"identity.resolve-dht-did" => self.handle_identity_resolve_dht_did(params).await,
|
||||
"identity.refresh-dht-did" => self.handle_identity_refresh_dht_did(params).await,
|
||||
"identity.dht-status" => self.handle_identity_dht_status(params).await,
|
||||
"identity.update-profile" => self.handle_identity_update_profile(params).await,
|
||||
"identity.publish-profile" => self.handle_identity_publish_profile(params).await,
|
||||
"identity.export-keys" => self.handle_identity_export_keys(params).await,
|
||||
"identity.create-nostr-key" => self.handle_identity_create_nostr_key(params).await,
|
||||
"identity.nostr-sign" => self.handle_identity_nostr_sign(params).await,
|
||||
"identity.nostr-encrypt-nip04" => self.handle_identity_nostr_encrypt_nip04(params).await,
|
||||
@@ -778,6 +807,7 @@ impl RpcHandler {
|
||||
// No 2FA: create a full session immediately
|
||||
let token = self.session_store.create().await;
|
||||
let csrf_token = generate_csrf_token();
|
||||
let remember_token = self.session_store.create_remember_token();
|
||||
response.headers_mut().append(
|
||||
"Set-Cookie",
|
||||
format!("session={}; HttpOnly; SameSite=Strict; Path=/{}", token, self.cookie_suffix())
|
||||
@@ -790,6 +820,13 @@ impl RpcHandler {
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
// Remember-me: HMAC-signed, survives backend restarts (30-day TTL)
|
||||
response.headers_mut().append(
|
||||
"Set-Cookie",
|
||||
format!("remember={}; HttpOnly; SameSite=Strict; Path=/; Max-Age={}{}", remember_token, REMEMBER_TTL, self.cookie_suffix())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,6 +881,23 @@ impl RpcHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// If session was auto-restored from remember-me, set new cookies on the response
|
||||
if let Some((new_session, new_csrf)) = new_session_cookies {
|
||||
let suffix = self.cookie_suffix();
|
||||
response.headers_mut().append(
|
||||
"Set-Cookie",
|
||||
format!("session={}; HttpOnly; SameSite=Strict; Path=/{}", new_session, suffix)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
response.headers_mut().append(
|
||||
"Set-Cookie",
|
||||
format!("csrf_token={}; SameSite=Strict; Path=/{}", new_csrf, suffix)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -864,13 +918,14 @@ fn generate_csrf_token() -> String {
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
/// Extract the csrf_token cookie value from headers.
|
||||
fn extract_csrf_cookie(headers: &hyper::HeaderMap) -> Option<String> {
|
||||
/// Extract a named cookie value from headers.
|
||||
fn extract_cookie(headers: &hyper::HeaderMap, name: &str) -> Option<String> {
|
||||
let prefix = format!("{}=", name);
|
||||
for value in headers.get_all("cookie") {
|
||||
if let Ok(s) = value.to_str() {
|
||||
for part in s.split(';') {
|
||||
let part = part.trim();
|
||||
if let Some(val) = part.strip_prefix("csrf_token=") {
|
||||
if let Some(val) = part.strip_prefix(&prefix) {
|
||||
let val = val.trim();
|
||||
if !val.is_empty() {
|
||||
return Some(val.to_string());
|
||||
@@ -882,6 +937,11 @@ fn extract_csrf_cookie(headers: &hyper::HeaderMap) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract the csrf_token cookie value from headers.
|
||||
fn extract_csrf_cookie(headers: &hyper::HeaderMap) -> Option<String> {
|
||||
extract_cookie(headers, "csrf_token")
|
||||
}
|
||||
|
||||
/// Extract the client IP from request headers (X-Real-IP or X-Forwarded-For).
|
||||
fn extract_client_ip(headers: &hyper::HeaderMap) -> IpAddr {
|
||||
headers
|
||||
|
||||
Reference in New Issue
Block a user