diff --git a/flake.nix b/flake.nix index 7396ebe..6f5a9c6 100644 --- a/flake.nix +++ b/flake.nix @@ -14,30 +14,48 @@ ] (system: f (import inputs.nixpkgs { inherit system; })); in { - packages = forAllSystems (pkgs: { - gitlab-job-status = pkgs.writeShellApplication { - name = "gitlab-job-status"; - runtimeInputs = [ - pkgs.curl - pkgs.jq - pkgs.git - pkgs.ncurses - pkgs.coreutils - ]; + packages = forAllSystems ( + pkgs: - text = builtins.readFile ./gitlab-job-status; - }; + let + lanshare = pkgs.rustPlatform.buildRustPackage (finalAttrs: { + pname = "lanshare"; + version = "0.1.0"; + src = ./lanshare; + cargoHash = "sha256-rUTARzVTbi0/KNFUDRynOTioIZIhiZf4suqb9UqtvV4="; - migrate-workspace-deps = pkgs.writeShellApplication { - name = "migrate-workspace-deps"; - runtimeInputs = [ - pkgs.jq - pkgs.git - pkgs.yq - ]; + meta = { + description = "A local screenshare application"; + mainProgram = "lanshare"; + }; + }); + in + { + inherit lanshare; + gitlab-job-status = pkgs.writeShellApplication { + name = "gitlab-job-status"; + runtimeInputs = [ + pkgs.curl + pkgs.jq + pkgs.git + pkgs.ncurses + pkgs.coreutils + ]; - text = builtins.readFile ./migrate-workspace-deps; - }; - }); + text = builtins.readFile ./gitlab-job-status; + }; + + migrate-workspace-deps = pkgs.writeShellApplication { + name = "migrate-workspace-deps"; + runtimeInputs = [ + pkgs.jq + pkgs.git + pkgs.yq + ]; + + text = builtins.readFile ./migrate-workspace-deps; + }; + } + ); }; } diff --git a/lanshare/src/main.rs b/lanshare/src/main.rs index bb53c99..b2b2afa 100644 --- a/lanshare/src/main.rs +++ b/lanshare/src/main.rs @@ -70,6 +70,15 @@ struct Args { /// connect via ws:// instead. #[arg(long, default_value_t = 8081, value_name = "PORT")] http_port: u16, + + /// Trust proxy headers (X-Forwarded-For, X-Real-IP) for client IPs + /// + /// Enable this when running behind a reverse proxy (e.g. nginx, caddy, + /// traefik). The server will use the IP from proxy headers instead of + /// the direct connection address. Do NOT enable this without a trusted + /// proxy, as clients can spoof these headers. + #[arg(long)] + trust_proxy: bool, } /// Messages sent between peers via the signaling server @@ -155,12 +164,14 @@ struct PeerState { /// Application state shared across all connections struct AppState { peers: DashMap, + trust_proxy: bool, } impl AppState { - fn new() -> Self { + fn new(trust_proxy: bool) -> Self { Self { peers: DashMap::new(), + trust_proxy, } } @@ -202,7 +213,7 @@ async fn main() { ) .init(); - let state = Arc::new(AppState::new()); + let state = Arc::new(AppState::new(args.trust_proxy)); let app = Router::new() .route("/", get(serve_index)) @@ -244,6 +255,9 @@ async fn main() { } } } + if args.trust_proxy { + println!(" \x1b[1;36mProxy mode:\x1b[0m trusting X-Forwarded-For / X-Real-IP headers"); + } println!(); println!(" \x1b[90mOpen this URL in browsers on your local network.\x1b[0m"); println!(" \x1b[90mPress Ctrl+C to stop the server.\x1b[0m"); @@ -290,14 +304,47 @@ async fn serve_index() -> impl IntoResponse { async fn ws_handler( ws: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, + headers: axum::http::HeaderMap, State(state): State>, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state, addr)) + let peer_ip = if state.trust_proxy { + extract_proxy_ip(&headers).unwrap_or_else(|| addr.ip().to_string()) + } else { + addr.ip().to_string() + }; + ws.on_upgrade(move |socket| handle_socket(socket, state, peer_ip)) } -async fn handle_socket(socket: WebSocket, state: Arc, addr: SocketAddr) { +/// Extract the client IP from proxy headers. +/// Checks X-Forwarded-For first (uses the leftmost/first IP), then X-Real-IP. +fn extract_proxy_ip(headers: &axum::http::HeaderMap) -> Option { + // X-Forwarded-For: client, proxy1, proxy2 + if let Some(forwarded_for) = headers.get("x-forwarded-for") { + if let Ok(value) = forwarded_for.to_str() { + if let Some(first_ip) = value.split(',').next() { + let trimmed = first_ip.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + } + + // X-Real-IP: client + if let Some(real_ip) = headers.get("x-real-ip") { + if let Ok(value) = real_ip.to_str() { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + + None +} + +async fn handle_socket(socket: WebSocket, state: Arc, peer_ip: String) { let peer_id = Uuid::new_v4().to_string(); - let peer_ip = addr.ip().to_string(); let (tx, _) = broadcast::channel::(64); // Add peer to state with default name