Skip to main content

crabshell_services/
greetd.rs

1//! greetd, the login manager, for a greeter config: a session is made for
2//! a user, its questions are answered, and it is started once the greeter
3//! exits. The protocol is greetd-ipc(7): over the socket `GREETD_SOCK`
4//! names, each message is its length as a native-endian `u32`, then that
5//! much JSON.
6
7#[cfg(test)]
8mod tests;
9
10use std::cell::RefCell;
11use std::collections::VecDeque;
12use std::io::{ErrorKind, Read, Write};
13use std::os::fd::AsFd;
14use std::os::unix::net::UnixStream;
15use std::rc::Rc;
16use std::sync::Once;
17
18use calloop::{Interest, RegistrationToken};
19use crabshell_reactive::{ReadSignal, Signal, untrack};
20use crabshell_ui::Cx;
21use serde::{Deserialize, Serialize};
22use serde_json::json;
23
24use crate::Shared;
25
26/// The largest message read from greetd; a larger one drops the
27/// connection.
28pub(crate) const MAX_MESSAGE: usize = 64 * 1024;
29
30/// The longest text kept from greetd, cut at a character.
31const MAX_TEXT: usize = 4 * 1024;
32
33/// The most informative messages kept; older ones go.
34pub(crate) const MAX_MESSAGES: usize = 64;
35
36/// Where a session being made stands.
37#[derive(Clone, Debug, Default, PartialEq)]
38pub enum GreetState {
39  /// No session is being made.
40  #[default]
41  Idle,
42  /// A request is on its way to greetd.
43  Busy,
44  /// greetd asks a question, such as for the password; `visible` is
45  /// false for a secret.
46  Asking {
47    /// The question, such as `Password:`.
48    message: String,
49    /// Whether the answer may show as it is typed.
50    visible: bool,
51  },
52  /// The user is authenticated, and the session can be started.
53  Authenticated,
54  /// The session will start once the greeter exits.
55  Started,
56  /// A request failed. `auth` says it was the credentials, which the
57  /// user can try again.
58  Failed {
59    /// greetd's description.
60    message: String,
61    /// Whether authentication failed, rather than something else.
62    auth: bool,
63  },
64}
65
66/// An informative or error message greetd sent while a session was made.
67#[derive(Clone, Debug, PartialEq)]
68pub struct GreetMessage {
69  /// The message.
70  pub text: String,
71  /// Whether it is an error, rather than information.
72  pub error: bool,
73}
74
75/// greetd, from `cx.greetd()`. A clone is cheap and shares it.
76///
77/// A greeter makes a session for a user with
78/// [`create_session`](Self::create_session), answers each question in
79/// [`state`](Self::state) with [`respond`](Self::respond), and once it is
80/// [`Authenticated`](GreetState::Authenticated) starts it with
81/// [`start_session`](Self::start_session), then exits so greetd can.
82#[derive(Clone)]
83pub struct Greetd {
84  /// `None` when the shell provided no services.
85  service: Option<Rc<Service>>,
86  state: ReadSignal<GreetState>,
87  messages: ReadSignal<Vec<GreetMessage>>,
88}
89
90impl Greetd {
91  /// Where the session being made stands.
92  pub fn state(&self) -> ReadSignal<GreetState> {
93    self.state
94  }
95
96  /// The informative and error messages greetd sent since the last
97  /// [`create_session`](Self::create_session), oldest first. They need no
98  /// answer: each is acknowledged as it comes.
99  pub fn messages(&self) -> ReadSignal<Vec<GreetMessage>> {
100    self.messages
101  }
102
103  /// Makes a session for `user` and starts authenticating them, ending
104  /// any session being made before.
105  pub fn create_session(&self, user: &str) {
106    if let Some(service) = &self.service {
107      service.create_session(user);
108    }
109  }
110
111  /// Answers greetd's question. Unless the state is
112  /// [`Asking`](GreetState::Asking), the answer is dropped and that is
113  /// logged. The answer is never logged, and its bytes are zeroed once
114  /// written.
115  pub fn respond(&self, answer: &str) {
116    if let Some(service) = &self.service {
117      service.respond(answer);
118    }
119  }
120
121  /// Starts the authenticated session with `command`, such as
122  /// `["sway"]`, and `env` lines such as `XDG_SESSION_TYPE=wayland`
123  /// added to what PAM sets. It starts once the greeter exits. Unless the
124  /// state is [`Authenticated`](GreetState::Authenticated), nothing is
125  /// sent and that is logged.
126  pub fn start_session(&self, command: Vec<String>, env: Vec<String>) {
127    if let Some(service) = &self.service {
128      service.start(command, env);
129    }
130  }
131
132  /// Ends the session being made.
133  pub fn cancel(&self) {
134    if let Some(service) = &self.service {
135      service.send(
136        Kind::Cancel { asked: true },
137        json!({"type": "cancel_session"}),
138      );
139    }
140  }
141}
142
143/// `cx.greetd()`.
144pub(crate) fn greetd(cx: &Cx) -> Greetd {
145  let Some(service) = cx.service::<Service>() else {
146    static LOGGED: Once = Once::new();
147    LOGGED.call_once(|| {
148      tracing::warn!("cx.greetd() needs the services the shell provides; it never connects");
149    });
150    return Greetd {
151      service: None,
152      state: cx.signal(GreetState::Idle).read_only(),
153      messages: cx.signal(Vec::new()).read_only(),
154    };
155  };
156  let (state, messages) = service.signals();
157  Greetd {
158    service: Some(service),
159    state: state.read_only(),
160    messages: messages.read_only(),
161  }
162}
163
164/// greetd for one shell: its signals and connection, made on first use.
165pub(crate) struct Service {
166  shared: Rc<Shared>,
167  inner: RefCell<Inner>,
168}
169
170#[derive(Default)]
171struct Inner {
172  signals: Option<(Signal<GreetState>, Signal<Vec<GreetMessage>>)>,
173  connection: Option<Connection>,
174  /// Requests waiting their turn: greetd answers one at a time.
175  queue: VecDeque<(Kind, Vec<u8>)>,
176  /// The request greetd is answering.
177  asked: Option<Kind>,
178  /// Whether greetd holds a session made here, which a new one must
179  /// cancel first.
180  session: bool,
181}
182
183struct Connection {
184  stream: UnixStream,
185  reader: RegistrationToken,
186  writer: Option<RegistrationToken>,
187  inbox: Vec<u8>,
188  /// The request being written, zeroed once all of it went.
189  outbox: Vec<u8>,
190  sent: usize,
191}
192
193/// What a request was, to read its answer.
194#[derive(Clone, Copy, Debug, PartialEq)]
195enum Kind {
196  Create,
197  Respond,
198  Start,
199  /// `asked` when the config asked; otherwise it ends a session that
200  /// failed, and changes nothing shown.
201  Cancel {
202    asked: bool,
203  },
204}
205
206/// greetd's answer to a request.
207#[derive(Deserialize)]
208#[serde(tag = "type", rename_all = "snake_case")]
209enum Reply {
210  Success,
211  Error {
212    error_type: String,
213    description: String,
214  },
215  AuthMessage {
216    auth_message_type: String,
217    auth_message: String,
218  },
219}
220
221impl Service {
222  pub(crate) fn new(shared: Rc<Shared>) -> Service {
223    Service {
224      shared,
225      inner: RefCell::default(),
226    }
227  }
228
229  /// The signals, made under the root on first use.
230  fn signals(&self) -> (Signal<GreetState>, Signal<Vec<GreetMessage>>) {
231    let mut inner = self.inner.borrow_mut();
232    *inner.signals.get_or_insert_with(|| {
233      (
234        self.shared.root.signal(GreetState::Idle),
235        self.shared.root.signal(Vec::new()),
236      )
237    })
238  }
239
240  fn create_session(self: &Rc<Self>, user: &str) {
241    if self.inner.borrow().session {
242      // greetd makes one session at a time.
243      self.send(
244        Kind::Cancel { asked: false },
245        json!({"type": "cancel_session"}),
246      );
247    }
248    let (_, messages) = self.signals();
249    set(messages, Vec::new());
250    self.send(
251      Kind::Create,
252      json!({"type": "create_session", "username": user}),
253    );
254  }
255
256  fn respond(self: &Rc<Self>, answer: &str) {
257    if !matches!(self.state(), GreetState::Asking { .. }) {
258      tracing::warn!("greetd asked nothing, so the answer is dropped");
259      return;
260    }
261    self.queue(Kind::Respond, answer_frame(Some(answer)), false);
262  }
263
264  fn start(self: &Rc<Self>, command: Vec<String>, env: Vec<String>) {
265    if self.state() != GreetState::Authenticated {
266      tracing::warn!("a session starts only once authenticated; nothing is sent");
267      return;
268    }
269    self.send(
270      Kind::Start,
271      json!({"type": "start_session", "cmd": command, "env": env}),
272    );
273  }
274
275  fn send(self: &Rc<Self>, kind: Kind, body: serde_json::Value) {
276    self.queue(kind, frame(body.to_string().as_bytes()), false);
277  }
278
279  fn state(&self) -> GreetState {
280    let (state, _) = self.signals();
281    untrack(|| state.try_with(Clone::clone)).unwrap_or_default()
282  }
283
284  /// Queues a request, at the `front` for one that follows up greetd's
285  /// last answer.
286  fn queue(self: &Rc<Self>, kind: Kind, bytes: Vec<u8>, front: bool) {
287    let (state, _) = self.signals();
288    if kind != (Kind::Cancel { asked: false }) {
289      set(state, GreetState::Busy);
290    }
291    let mut inner = self.inner.borrow_mut();
292    match kind {
293      Kind::Create => inner.session = true,
294      Kind::Cancel { .. } => inner.session = false,
295      Kind::Respond | Kind::Start => {}
296    }
297    if front {
298      inner.queue.push_front((kind, bytes));
299    } else {
300      inner.queue.push_back((kind, bytes));
301    }
302    drop(inner);
303    self.next();
304  }
305
306  /// Sends the next request, once greetd answered the last.
307  fn next(self: &Rc<Self>) {
308    {
309      let inner = self.inner.borrow();
310      if inner.asked.is_some() || inner.queue.is_empty() {
311        return;
312      }
313    }
314    if self.inner.borrow().connection.is_none() && !self.connect() {
315      return;
316    }
317    let mut inner = self.inner.borrow_mut();
318    let Some((kind, mut bytes)) = inner.queue.pop_front() else {
319      return;
320    };
321    inner.asked = Some(kind);
322    let connection = inner
323      .connection
324      .as_mut()
325      .expect("invariant: connected above");
326    if connection.outbox.is_empty() {
327      connection.outbox = bytes;
328      connection.sent = 0;
329    } else {
330      connection.outbox.extend_from_slice(&bytes);
331      zero(&mut bytes);
332    }
333    drop(inner);
334    self.flush();
335  }
336
337  /// Connects to greetd. `false`, with the state failed and the queue
338  /// dropped, if it isn't there.
339  fn connect(self: &Rc<Self>) -> bool {
340    let Some(event_loop) = self.shared.event_loop() else {
341      return false;
342    };
343    let path = self.shared.config.greetd.clone();
344    let opened = path
345      .as_ref()
346      .ok_or_else(|| std::io::Error::other("GREETD_SOCK isn't set"))
347      .and_then(|path| {
348        let stream = UnixStream::connect(path)?;
349        stream.set_nonblocking(true)?;
350        let fd = stream.as_fd().try_clone_to_owned()?;
351        Ok((stream, fd))
352      });
353    let (stream, fd) = match opened {
354      Ok(opened) => opened,
355      Err(error) => {
356        let path = path.as_ref().map(|path| path.display().to_string());
357        tracing::warn!(?path, %error, "can't reach greetd");
358        self.fail(format!("can't reach greetd: {error}"), false);
359        return false;
360      }
361    };
362    let service = Rc::clone(self);
363    let Some(reader) = event_loop.insert_fd(fd, Interest::READ, Box::new(move || service.read()))
364    else {
365      self.fail(String::from("can't follow greetd's socket"), false);
366      return false;
367    };
368    self.inner.borrow_mut().connection = Some(Connection {
369      stream,
370      reader,
371      writer: None,
372      inbox: Vec::new(),
373      outbox: Vec::new(),
374      sent: 0,
375    });
376    true
377  }
378
379  /// Writes what the socket takes, zeroing what went, and waits for room
380  /// for the rest.
381  fn flush(self: &Rc<Self>) {
382    let Some(event_loop) = self.shared.event_loop() else {
383      return;
384    };
385    let mut inner = self.inner.borrow_mut();
386    let Some(connection) = inner.connection.as_mut() else {
387      return;
388    };
389    let mut broken = None;
390    while connection.sent < connection.outbox.len() {
391      match connection
392        .stream
393        .write(&connection.outbox[connection.sent..])
394      {
395        Ok(written) => connection.sent += written,
396        Err(error) if error.kind() == ErrorKind::Interrupted => {}
397        Err(error) if error.kind() == ErrorKind::WouldBlock => break,
398        Err(error) => {
399          broken = Some(error.to_string());
400          break;
401        }
402      }
403    }
404    if connection.sent == connection.outbox.len() {
405      zero(&mut connection.outbox);
406      connection.outbox.clear();
407      connection.sent = 0;
408    }
409    if broken.is_none() {
410      if connection.outbox.is_empty() {
411        if let Some(writer) = connection.writer.take() {
412          event_loop.remove(writer);
413        }
414      } else if connection.writer.is_none() {
415        match connection.stream.as_fd().try_clone_to_owned() {
416          Ok(fd) => {
417            let service = Rc::clone(self);
418            let write = Box::new(move || service.flush());
419            connection.writer = event_loop.insert_fd(fd, Interest::WRITE, write);
420          }
421          Err(error) => broken = Some(error.to_string()),
422        }
423      }
424    }
425    drop(inner);
426    if let Some(why) = broken {
427      self.lost(&why);
428    }
429  }
430
431  /// Reads what greetd sent and acts on each whole message.
432  fn read(self: &Rc<Self>) {
433    let mut chunk = [0; 16 * 1024];
434    let mut inner = self.inner.borrow_mut();
435    let Some(connection) = inner.connection.as_mut() else {
436      return;
437    };
438    let why = match connection.stream.read(&mut chunk) {
439      Ok(0) => Some(String::from("greetd closed the socket")),
440      Ok(read) => {
441        connection.inbox.extend_from_slice(&chunk[..read]);
442        None
443      }
444      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
445        return;
446      }
447      Err(error) => Some(error.to_string()),
448    };
449    if let Some(why) = why {
450      drop(inner);
451      return self.lost(&why);
452    }
453    let mut replies = Vec::new();
454    let mut broken = None;
455    while connection.inbox.len() >= 4 {
456      let length = u32::from_ne_bytes(connection.inbox[..4].try_into().expect("four bytes"));
457      let length = length as usize;
458      if length > MAX_MESSAGE {
459        broken = Some(format!("greetd sent a message of {length} bytes"));
460        break;
461      }
462      if connection.inbox.len() < 4 + length {
463        break;
464      }
465      let body: Vec<u8> = connection.inbox.drain(..4 + length).skip(4).collect();
466      replies.push(body);
467    }
468    drop(inner);
469    if let Some(why) = broken {
470      return self.lost(&why);
471    }
472    for body in replies {
473      match serde_json::from_slice::<Reply>(&body) {
474        Ok(reply) => self.answered(reply),
475        Err(error) => {
476          tracing::warn!(%error, "a message greetd sent doesn't parse");
477          return self.lost("greetd sent something it shouldn't");
478        }
479      }
480    }
481  }
482
483  /// Acts on greetd's answer to the request it was answering.
484  fn answered(self: &Rc<Self>, reply: Reply) {
485    let (state, messages) = self.signals();
486    let (asked, superseded) = {
487      let mut inner = self.inner.borrow_mut();
488      let cancel = |(kind, _): &(Kind, Vec<u8>)| matches!(kind, Kind::Cancel { .. });
489      (inner.asked.take(), inner.queue.iter().any(cancel))
490    };
491    let Some(kind) = asked else {
492      tracing::warn!("greetd answered nothing asked");
493      return;
494    };
495    if superseded && !matches!(kind, Kind::Cancel { .. }) {
496      // A queued cancel ends this attempt, so its answers show nothing.
497      return self.next();
498    }
499    match (kind, reply) {
500      // A cancel's error means greetd had no session left to end.
501      (Kind::Cancel { asked }, _) => {
502        if asked {
503          set(state, GreetState::Idle);
504        }
505      }
506      (Kind::Create | Kind::Respond, Reply::Success) => set(state, GreetState::Authenticated),
507      (Kind::Start, Reply::Success) => set(state, GreetState::Started),
508      (
509        kind,
510        Reply::Error {
511          error_type,
512          description,
513        },
514      ) => {
515        let auth = error_type == "auth_error";
516        set(
517          state,
518          GreetState::Failed {
519            message: cut(description),
520            auth,
521          },
522        );
523        // A failed attempt is ended, so another can begin.
524        if matches!(kind, Kind::Create | Kind::Respond) {
525          let cancel = json!({"type": "cancel_session"});
526          let bytes = frame(cancel.to_string().as_bytes());
527          self.queue(Kind::Cancel { asked: false }, bytes, true);
528        }
529      }
530      (
531        _,
532        Reply::AuthMessage {
533          auth_message_type,
534          auth_message,
535        },
536      ) => match auth_message_type.as_str() {
537        kind @ ("visible" | "secret") => set(
538          state,
539          GreetState::Asking {
540            message: cut(auth_message),
541            visible: kind == "visible",
542          },
543        ),
544        kind => {
545          let message = GreetMessage {
546            text: cut(auth_message),
547            error: kind == "error",
548          };
549          messages.try_update(|all| {
550            if all.len() == MAX_MESSAGES {
551              all.remove(0);
552            }
553            all.push(message);
554          });
555          // Informative: acknowledged with no answer, before anything else.
556          self.queue(Kind::Respond, answer_frame(None), true);
557        }
558      },
559    }
560    self.next();
561  }
562
563  /// The request in flight failed for `message`.
564  fn fail(&self, message: String, auth: bool) {
565    let (state, _) = self.signals();
566    set(state, GreetState::Failed { message, auth });
567    let mut inner = self.inner.borrow_mut();
568    for (_, bytes) in &mut inner.queue {
569      zero(bytes);
570    }
571    inner.queue.clear();
572    inner.asked = None;
573    inner.session = false;
574  }
575
576  /// Drops the connection, which ends the session being made.
577  fn lost(&self, why: &str) {
578    tracing::warn!(why, "lost the connection to greetd");
579    let connection = self.inner.borrow_mut().connection.take();
580    if let (Some(mut connection), Some(event_loop)) = (connection, self.shared.event_loop()) {
581      event_loop.remove(connection.reader);
582      if let Some(writer) = connection.writer {
583        event_loop.remove(writer);
584      }
585      zero(&mut connection.outbox);
586    }
587    self.fail(format!("lost greetd: {why}"), false);
588  }
589}
590
591/// `body` with its length before it, as greetd reads it.
592fn frame(body: &[u8]) -> Vec<u8> {
593  let length = u32::try_from(body.len()).unwrap_or(u32::MAX);
594  let mut framed = Vec::with_capacity(4 + body.len());
595  framed.extend_from_slice(&length.to_ne_bytes());
596  framed.extend_from_slice(body);
597  framed
598}
599
600/// An answer framed for greetd, written straight into one buffer sized so
601/// it never grows: no copy of the answer is left unzeroed.
602fn answer_frame(answer: Option<&str>) -> Vec<u8> {
603  #[derive(Serialize)]
604  struct Answer<'a> {
605    #[serde(rename = "type")]
606    kind: &'static str,
607    response: Option<&'a str>,
608  }
609  // JSON writes a byte as at most six.
610  let mut bytes = Vec::with_capacity(64 + 6 * answer.map_or(0, str::len));
611  bytes.extend_from_slice(&[0; 4]);
612  let answer = Answer {
613    kind: "post_auth_message_response",
614    response: answer,
615  };
616  serde_json::to_writer(&mut bytes, &answer).expect("invariant: a string serializes");
617  let length = u32::try_from(bytes.len() - 4).unwrap_or(u32::MAX);
618  bytes[..4].copy_from_slice(&length.to_ne_bytes());
619  bytes
620}
621
622/// Overwrites `bytes` with zeros, as for an answer once sent.
623fn zero(bytes: &mut [u8]) {
624  for byte in bytes.iter_mut() {
625    *byte = 0;
626  }
627  std::hint::black_box(bytes);
628}
629
630/// `text` cut to [`MAX_TEXT`] bytes at a character.
631fn cut(mut text: String) -> String {
632  if text.len() > MAX_TEXT {
633    text.truncate(text.floor_char_boundary(MAX_TEXT));
634  }
635  text
636}
637
638/// Sets `signal` unless it holds `value` or is gone.
639fn set<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
640  if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
641    signal.try_set(value);
642  }
643}