From 4077ff0949c4ef7220e19fe7e6a8cac3e0ff69d2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 2 Oct 2023 14:38:00 -0600 Subject: [PATCH] Partially implement greetd IPC --- Cargo.lock | 44 ++++++++ Cargo.toml | 8 +- examples/server.rs | 55 ++++++++++ src/main.rs | 255 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 examples/server.rs diff --git a/Cargo.lock b/Cargo.lock index 6beff70..7613057 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,9 +651,12 @@ dependencies = [ name = "cosmic-greeter" version = "0.1.0" dependencies = [ + "env_logger", "greetd_ipc", "libcosmic", + "log", "tokio", + "uzers", ] [[package]] @@ -968,6 +971,19 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1450,6 +1466,7 @@ checksum = "839390036de887ed0e6a58a82fc03619b27c96f24ac6425f7c9a6c397a6482f6" dependencies = [ "serde", "serde_json", + "thiserror", ] [[package]] @@ -1530,6 +1547,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iced" version = "0.10.0" @@ -1813,6 +1836,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.14", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3667,6 +3701,16 @@ dependencies = [ "tiny-skia-path", ] +[[package]] +name = "uzers" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +dependencies = [ + "libc", + "log", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 8891782..26cc32a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] -greetd_ipc = "0.9" +env_logger = "0.10" +log = "0.4" +uzers = "0.11" + +[dependencies.greetd_ipc] +version = "0.9" +features = ["sync-codec"] [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" diff --git a/examples/server.rs b/examples/server.rs new file mode 100644 index 0000000..db4bc54 --- /dev/null +++ b/examples/server.rs @@ -0,0 +1,55 @@ +use greetd_ipc::{codec::SyncCodec, AuthMessageType, Request, Response}; +use std::io; +use tokio::net::UnixListener; + +#[tokio::main] +async fn main() { + let listener = UnixListener::bind("socket").unwrap(); + println!("listening"); + + loop { + let (socket, _addr) = listener.accept().await.unwrap(); + println!("new connection"); + + loop { + let request = { + socket.readable().await.unwrap(); + + let mut bytes = Vec::with_capacity(4096); + match socket.try_read_buf(&mut bytes) { + Ok(0) => break, + Ok(count) => { + println!("read {} bytes", count); + } + Err(err) => match err.kind() { + io::ErrorKind::WouldBlock => continue, + _ => { + println!("failed to read socket: {:?}", err); + break; + } + }, + } + + let mut cursor = io::Cursor::new(bytes); + Request::read_from(&mut cursor).unwrap() + }; + println!("{:?}", request); + + let response = match request { + Request::CreateSession { username } => Response::AuthMessage { + auth_message_type: AuthMessageType::Secret, + auth_message: "Password:".to_string(), + }, + Request::PostAuthMessageResponse { response } => Response::Success, + _ => { + println!("unhandled request"); + break; + } + }; + + let mut bytes = Vec::with_capacity(4096); + response.write_to(&mut bytes).unwrap(); + socket.try_write(&bytes).unwrap(); + } + } +} diff --git a/src/main.rs b/src/main.rs index 4bf8df2..8e14dcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,35 @@ //! Application API example use cosmic::app::{message, Command, Core, Settings}; -use cosmic::prelude::*; -use cosmic::{executor, iced, widget, ApplicationExt, Element}; +use cosmic::{executor, iced, widget, Element}; +use greetd_ipc::{codec::SyncCodec, AuthMessageType, Request, Response}; use std::{env, io, sync::Arc}; -use tokio::fs::{File, OpenOptions}; +use tokio::net::UnixStream; +use uzers::os::unix::UserExt; fn main() -> Result<(), Box> { + env_logger::init(); + + // This method is unsafe due to using global state (libc pwent functions). + let users: Vec = unsafe { + uzers::all_users() + .filter(|user| { + if user.uid() < 1000 { + // Skip system accounts + return false; + } + + match user.shell().file_name().and_then(|x| x.to_str()) { + // Skip shell ending in false + Some("false") => false, + // Skip shell ending in nologin + Some("nologin") => false, + _ => true, + } + }) + .collect() + }; + let settings = Settings::default() .antialiasing(true) .client_decorations(true) @@ -19,7 +42,7 @@ fn main() -> Result<(), Box> { .scale_factor(1.0) .theme(cosmic::Theme::dark()); - cosmic::app::run::(settings, ())?; + cosmic::app::run::(settings, users)?; Ok(()) } @@ -29,25 +52,101 @@ pub enum SocketState { /// Opening GREETD_SOCK Pending, /// GREETD_SOCK is open - Open(Arc), + Open(Arc), /// No GREETD_SOCK variable set NotSet, /// Failed to open GREETD_SOCK Error(Arc), } +#[derive(Clone, Debug)] +pub enum InputState { + None, + Username(String), + Auth(bool, String, String), +} + /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { + None, Socket(SocketState), - Username(String), + Input(InputState), + Submit, + Login, +} + +async fn request(socket: Arc, request: Request) -> Message { + //TODO: handle errors + socket.writable().await.unwrap(); + { + let mut bytes = Vec::::new(); + request.write_to(&mut bytes).unwrap(); + socket.try_write(&bytes).unwrap(); + } + + //TODO: handle responses at any time? + loop { + socket.readable().await.unwrap(); + + let mut bytes = Vec::::with_capacity(4096); + match socket.try_read_buf(&mut bytes) { + Ok(0) => break, + Ok(count) => { + log::info!("read {} bytes", count); + + let mut cursor = io::Cursor::new(bytes); + let response = Response::read_from(&mut cursor).unwrap(); + log::info!("{:?}", response); + match response { + Response::AuthMessage { + auth_message_type, + auth_message, + } => match auth_message_type { + AuthMessageType::Secret => { + return Message::Input(InputState::Auth( + true, + auth_message, + String::new(), + )) + } + AuthMessageType::Visible => { + return Message::Input(InputState::Auth( + false, + auth_message, + String::new(), + )) + } + _ => todo!("unsupported auth_message_type {:?}", auth_message_type), + }, + Response::Success => { + return Message::Login; + } + _ => { + log::error!("unhandled response"); + break; + } + } + } + Err(err) => match err.kind() { + io::ErrorKind::WouldBlock => continue, + _ => { + log::error!("failed to read socket: {:?}", err); + break; + } + }, + } + } + + Message::None } /// The [`App`] stores application-specific state. pub struct App { core: Core, + users: Vec, socket_state: SocketState, - username: String, + input_state: InputState, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -56,7 +155,7 @@ impl cosmic::Application for App { type Executor = executor::Default; /// Argument received [`cosmic::Application::new`]. - type Flags = (); + type Flags = Vec; /// Message type specific to our [`App`]. type Message = Message; @@ -73,7 +172,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(mut core: Core, _flags: Self::Flags) -> (Self, Command) { + fn init(mut core: Core, users: Self::Flags) -> (Self, Command) { core.window.show_window_menu = false; core.window.show_headerbar = false; core.window.sharp_corners = true; @@ -84,18 +183,15 @@ impl cosmic::Application for App { ( App { core, + users, socket_state: SocketState::Pending, - username: String::new(), + //TODO: set to pending until socket is open? + input_state: InputState::Username(String::new()), }, Command::perform( async { message::app(Message::Socket(match env::var_os("GREETD_SOCK") { - Some(socket_path) => match OpenOptions::new() - .read(true) - .write(true) - .open(&socket_path) - .await - { + Some(socket_path) => match UnixStream::connect(&socket_path).await { Ok(socket) => SocketState::Open(Arc::new(socket)), Err(err) => SocketState::Error(Arc::new(err)), }, @@ -110,33 +206,132 @@ impl cosmic::Application for App { /// Handle application events here. fn update(&mut self, message: Self::Message) -> Command { match message { + Message::None => {} Message::Socket(socket_state) => { self.socket_state = socket_state; } - Message::Username(username) => { - self.username = username; + Message::Input(input_state) => { + self.input_state = input_state; } + Message::Submit => match &self.socket_state { + SocketState::Open(socket) => match &self.input_state { + InputState::None => {} + InputState::Username(username) => { + let socket = socket.clone(); + let username = username.clone(); + return Command::perform( + async move { + message::app( + request(socket, Request::CreateSession { username }).await, + ) + }, + |x| x, + ); + } + InputState::Auth(_secret, _prompt, value) => { + let socket = socket.clone(); + let value = value.clone(); + return Command::perform( + async move { + message::app( + request( + socket, + Request::PostAuthMessageResponse { + response: Some(value), + }, + ) + .await, + ) + }, + |x| x, + ); + } + }, + _ => todo!("socket not open but input provided"), + }, + Message::Login => match &self.socket_state { + SocketState::Open(socket) => { + let socket = socket.clone(); + return Command::perform( + async move { + message::app( + request( + socket, + //TODO: get session information from /usr/share/wayland-sessions + Request::StartSession { + cmd: vec!["start-cosmic".to_string()], + env: vec![], + }, + ) + .await, + ) + }, + |x| x, + ); + } + _ => todo!("socket not open but attempting to log in"), + }, } Command::none() } /// Creates a view after each update. fn view(&self) -> Element { - let text = widget::text(match &self.socket_state { - SocketState::Pending => format!("Opening GREETD_SOCK"), - SocketState::Open(_) => format!("GREETD_SOCK open"), - SocketState::NotSet => format!("GREETD_SOCK variable not set"), - SocketState::Error(err) => format!("Failed to open GREETD_SOCK: {:?}", err), - }); + let mut column = widget::column::with_capacity(2); - let column = widget::column::with_capacity(3) - .push(text) - .push(widget::text("Username:")) - .push(widget::text_input("Username", &self.username).on_input(Message::Username)); + match &self.socket_state { + SocketState::Pending => { + column = column.push(widget::text("Opening GREETD_SOCK")); + } + SocketState::Open(_) => match &self.input_state { + InputState::None => {} + InputState::Username(username) => { + column = column.push(widget::text("Username:")); + column = column.push( + widget::text_input("Username", &username) + .on_input(|username| Message::Input(InputState::Username(username))) + .on_submit(Message::Submit), + ); - let centered = widget::container(column) + for user in &self.users { + match user.name().to_str() { + Some(user_name) => { + column = column.push(widget::button(user_name).on_press( + Message::Input(InputState::Username(user_name.to_string())), + )); + } + None => {} + } + } + } + InputState::Auth(secret, prompt, value) => { + column = column.push(widget::text(prompt)); + let text_input = widget::text_input("", &value) + .on_input(|value| { + Message::Input(InputState::Auth(*secret, prompt.clone(), value)) + }) + .on_submit(Message::Submit); + if *secret { + column = column.push(text_input.password()); + } else { + column = column.push(text_input); + } + } + }, + SocketState::NotSet => { + column = column.push(widget::text("GREETD_SOCK variable not set")); + } + SocketState::Error(err) => { + column = column.push(widget::text(format!( + "Failed to open GREETD_SOCK: {:?}", + err + ))) + } + } + + let centered = widget::container(column.spacing(12.0).width(iced::Length::Fixed(400.0))) .width(iced::Length::Fill) - .height(iced::Length::Shrink) + .height(iced::Length::Fill) .align_x(iced::alignment::Horizontal::Center) .align_y(iced::alignment::Vertical::Center);