diff --git a/crates/biliup-cli/src/cli.rs b/crates/biliup-cli/src/cli.rs index 2bbae12..fd5eb21 100644 --- a/crates/biliup-cli/src/cli.rs +++ b/crates/biliup-cli/src/cli.rs @@ -170,6 +170,11 @@ pub enum Commands { #[arg(long, default_value = "false")] auth: bool, + /// 为会话 Cookie 附加 Secure 属性。仅当通过 HTTPS 反向代理访问 Web UI 时开启; + /// 直接通过 HTTP 远程访问时开启会导致浏览器丢弃登录态 + #[arg(long, default_value = "false")] + secure_session_cookie: bool, + /// 使用 biliup 1.0.7 风格配置文件启动录制 #[arg(short, long, value_name = "FILE")] config: Option, @@ -228,7 +233,12 @@ mod tests { assert_eq!(cli.user_cookie, Path::new("cookies.json")); assert!(matches!( cli.command, - Commands::Server { ref bind, auth: false, .. } if bind == "127.0.0.1" + Commands::Server { + ref bind, + auth: false, + secure_session_cookie: false, + .. + } if bind == "127.0.0.1" )); } diff --git a/crates/biliup-cli/src/lib.rs b/crates/biliup-cli/src/lib.rs index ba92f19..60d3c71 100644 --- a/crates/biliup-cli/src/lib.rs +++ b/crates/biliup-cli/src/lib.rs @@ -38,6 +38,7 @@ pub async fn run( run_with_cookie( addr, auth, + false, log_handle, config_path, PathBuf::from("cookies.json"), @@ -48,6 +49,7 @@ pub async fn run( pub async fn run_with_cookie( addr: (&str, u16), auth: bool, + secure_session_cookie: bool, log_handle: LogHandle, config_path: Option, user_cookie: PathBuf, @@ -111,7 +113,7 @@ pub async fn run_with_cookie( } tracing::info!("migrations successfully ran, initializing axum server..."); - ApplicationController::serve(&addr, auth, service_register) + ApplicationController::serve(&addr, auth, secure_session_cookie, service_register) .await .attach("could not initialize application routes")?; Ok(()) diff --git a/crates/biliup-cli/src/main.rs b/crates/biliup-cli/src/main.rs index 61941f3..5579ab9 100644 --- a/crates/biliup-cli/src/main.rs +++ b/crates/biliup-cli/src/main.rs @@ -154,11 +154,13 @@ async fn main() -> AppResult<()> { bind, port, auth, + secure_session_cookie, config, } => { biliup_cli::run_with_cookie( (&bind, port), auth, + secure_session_cookie, console_reload_handle, config, user_cookie, diff --git a/crates/biliup-cli/src/server/app.rs b/crates/biliup-cli/src/server/app.rs index 1c6192a..f7d5c85 100644 --- a/crates/biliup-cli/src/server/app.rs +++ b/crates/biliup-cli/src/server/app.rs @@ -28,6 +28,7 @@ impl ApplicationController { pub async fn serve( addr: &SocketAddr, enable_login_guard: bool, + secure_session_cookie: bool, service_register: ServiceRegister, ) -> AppResult<()> { // 会话层配置 @@ -50,10 +51,13 @@ impl ApplicationController { // 配置会话管理层 let session_layer = SessionManagerLayer::new(session_store) - // Loopback development uses plain HTTP. A non-loopback bind is - // expected to sit behind TLS, so never send its session cookie over - // an insecure browser connection. - .with_secure(!addr.ip().is_loopback()) + // The server itself only speaks plain HTTP, and browsers refuse to + // store `Secure` cookies delivered over insecure remote origins, so + // a forced `Secure` attribute on non-loopback binds silently broke + // every direct-HTTP login. Default to a non-`Secure` cookie and let + // deployments behind an HTTPS reverse proxy opt in explicitly via + // `--secure-session-cookie`. + .with_secure(secure_session_cookie) .with_name("biliup.sid") .with_expiry(Expiry::OnInactivity(Duration::days(7))); // .with_signed(key); @@ -167,11 +171,12 @@ async fn shutdown_signal( #[cfg(test)] mod tests { use super::with_optional_auth; + use crate::server::api::auth; use crate::server::infrastructure::connection_pool::ConnectionManager; use crate::server::infrastructure::users::Backend; use axum::Router; use axum::body::Body; - use axum::http::{Request, StatusCode}; + use axum::http::{Request, StatusCode, header}; use axum::routing::get; use axum_login::AuthManagerLayerBuilder; use tower::ServiceExt; @@ -210,4 +215,60 @@ mod tests { assert_eq!(request_log_route(true).await, StatusCode::UNAUTHORIZED); assert_eq!(request_log_route(false).await, StatusCode::OK); } + + /// Issues a real login-session `Set-Cookie` through the register endpoint + /// and returns the raw header value. + async fn session_set_cookie_header(secure_session_cookie: bool) -> String { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("data.sqlite3"); + let pool = ConnectionManager::new_pool(db.to_str().unwrap()) + .await + .unwrap(); + let session_store = SqliteStore::new(pool.clone()); + session_store.migrate().await.unwrap(); + let auth_layer = AuthManagerLayerBuilder::new( + Backend::new(pool), + SessionManagerLayer::new(session_store).with_secure(secure_session_cookie), + ) + .build(); + let app = auth::router().layer(auth_layer); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/users/register") + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from( + r#"{"username":"biliup","password":"test-password"}"#, + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + response + .headers() + .get(header::SET_COOKIE) + .expect("login must establish a session cookie") + .to_str() + .unwrap() + .to_string() + } + + /// Direct HTTP access (the default deployment) must not mark the session + /// cookie `Secure`, otherwise browsers on remote non-HTTPS origins drop it + /// and every login silently bounces back to the login page. + #[tokio::test] + async fn session_cookie_secure_attribute_follows_configuration() { + assert!( + !session_set_cookie_header(false).await.contains("Secure"), + "default session cookie must work over plain HTTP" + ); + assert!( + session_set_cookie_header(true).await.contains("Secure"), + "--secure-session-cookie must mark the cookie Secure" + ); + } } diff --git a/crates/stream-gears/src/server.rs b/crates/stream-gears/src/server.rs index be421f6..d860e65 100644 --- a/crates/stream-gears/src/server.rs +++ b/crates/stream-gears/src/server.rs @@ -284,11 +284,13 @@ pub(crate) async fn _main(args: &[String]) -> AppResult<()> { bind, port, auth, + secure_session_cookie, config, } => { biliup_cli::run_with_cookie( (&bind, port), auth, + secure_session_cookie, reload_handle, config, cli.user_cookie,