motd/
lib.rs

1/*!
2The motd crate exposes a mechanism for dumping the current MOTD
3on linux. In order to work around some issues with how pam_motd.so
4handles permissions, it must re-exec the current binary to make
5use of the LD_PRELOAD trick. You must make sure that your binary
6can handle this re-execing by registering `motd::handle_reexec()`
7in your main function. It is a no-op unless a few magic environment
8variables have been set, so you don't need to worry about it impacting
9the way your binary behaves otherwise.
10
11Your main should look like this:
12
13```
14# #[cfg(not(feature = "socall"))]
15# fn main() {}
16# #[cfg(feature = "socall")]
17fn main() {
18    motd::handle_reexec();
19
20    // ...
21}
22```
23
24then elsewhere in your code you can call value to get
25the motd message like
26
27```
28# #[cfg(not(feature = "socall"))]
29# fn main() {}
30# #[cfg(feature = "socall")]
31# fn main() -> Result<(), motd::Error> {
32# motd::handle_reexec();
33let motd_resolver = motd::Resolver::new(motd::PamMotdResolutionStrategy::Auto)?;
34let motd_msg = motd_resolver.value(motd::ArgResolutionStrategy::Auto)?;
35# Ok(())
36# }
37```
38
39By default, motd finds and calls the pam_motd.so file that is the source
40of truth implementation for motd resolution, but it also contains a pure
41rust reimplementation of the motd resolution logic. This implementation has
42the potential to diverge from the behavior of pam_motd.so, but it contains
430 unsafe rust and does a fairly good job. You can switch to this mode by
44disabling default features for the crate. This will make the `so_finder`
45argument to `Resolver::new` disapear and remove the need to register the
46`handle_reexec` handler in your main function. You can then use it
47like
48
49```
50# #[cfg(feature = "socall")]
51# fn main() {}
52# #[cfg(not(feature = "socall"))]
53# fn main() -> Result<(), motd::Error> {
54let motd_resolver = motd::Resolver::new()?;
55let motd_msg = motd_resolver.value(motd::ArgResolutionStrategy::Auto)?;
56# Ok(())
57# }
58```
59*/
60
61use std::{
62    fmt::Debug,
63    fs, io,
64    io::BufRead,
65    path::{Path, PathBuf},
66};
67
68use log::warn;
69use serde_derive::{Deserialize, Serialize};
70
71#[cfg(feature = "socall")]
72mod socall;
73#[cfg(feature = "socall")]
74pub use socall::handle_reexec;
75#[cfg(feature = "socall")]
76pub use socall::PamMotdResolutionStrategy;
77#[cfg(feature = "socall")]
78pub use socall::Resolver;
79
80#[cfg(not(feature = "socall"))]
81mod reimpl;
82#[cfg(not(feature = "socall"))]
83pub use reimpl::Resolver;
84
85const PAM_DIR: [&str; 2] = ["/etc", "pam.d"];
86
87macro_rules! merr {
88    ($($arg:tt)*) => {{
89        Error::Err { msg: format!($($arg)*) }
90    }}
91}
92
93/// The strategy to use to determine which args should be passed to `pam_motd.so`.
94/// pam configuration often includes arguments to various pam modules, and `pam_motd.so`
95/// is one such module. You likely want to match the args that the config passes into
96/// the module.
97///
98/// In all cases, the "noupdate" arg will be included since without it debian flavored
99/// `pam_motd.so`s will fail for want of write permissions on the motd file. Non-debian
100/// `pam_motd.so`s just write an error to syslog and trundle along for unknown args, so
101/// this should not cause an issue in general.
102#[derive(Debug, Clone, Deserialize, Serialize)]
103pub enum ArgResolutionStrategy {
104    /// Pass the exact arg vector given with not parsing or resolution.
105    Exact(Vec<String>),
106    /// Parse the given service files (found in `/etc/pam.d/{service}`) looking for
107    /// `pam_motd.so` entries and slurping any `motd=` or `motd_dir=` arguments. Multiple entries
108    /// combine args into a single arg list. Afterwards, the args are deduped. If the service
109    /// does not have a file, it is ignored.
110    MatchServices(Vec<String>),
111    /// A good default. Equivalent to `MatchServices(vec!["ssh", "login"])`
112    Auto,
113}
114
115impl ArgResolutionStrategy {
116    fn resolve(self) -> Result<Vec<String>, Error> {
117        match self {
118            ArgResolutionStrategy::Exact(args) => Ok(args),
119            ArgResolutionStrategy::Auto => ArgResolutionStrategy::MatchServices(vec![
120                String::from("ssh"),
121                String::from("login"),
122            ])
123            .resolve(),
124            ArgResolutionStrategy::MatchServices(services) => {
125                let mut args = vec![];
126                for service in services.into_iter() {
127                    let mut service_path = PathBuf::new();
128                    for part in PAM_DIR.iter() {
129                        service_path.push(part);
130                    }
131                    service_path.push(service);
132
133                    args.extend(Self::slurp_args(service_path)?);
134                }
135
136                // remove duplicates since parsing multiple service files means we probably
137                // have some.
138                args.sort_unstable();
139                args.dedup();
140
141                // make sure the debian variant still works
142                args.push(String::from("noupdate"));
143
144                Ok(args)
145            }
146        }
147    }
148
149    fn slurp_args<P: AsRef<Path> + Debug>(service_file: P) -> Result<Vec<String>, Error> {
150        if !service_file.as_ref().is_file() {
151            // ignore any missing services
152            return Ok(vec![]);
153        }
154
155        let file = fs::File::open(&service_file)
156            .map_err(|e| merr!("opening {:?} to parse args: {:?}", &service_file, e))?;
157        let reader = io::BufReader::new(file);
158
159        let mut args = vec![];
160        for line in reader.lines() {
161            let line = line.map_err(|e| merr!("reading line from {:?}: {:?}", &service_file, e))?;
162            let line = line.trim();
163            if line.starts_with('#') || line.is_empty() {
164                continue;
165            }
166
167            if line.starts_with("@include") {
168                // we need to recursively parse the included service
169                let parts: Vec<&str> = line.split_whitespace().collect();
170                if parts.len() != 2 {
171                    warn!(
172                        "expect exactly 1 argument to @include, got {}",
173                        parts.len() - 1
174                    );
175                }
176
177                let mut included_service_path = PathBuf::new();
178                for part in PAM_DIR.iter() {
179                    included_service_path.push(part);
180                }
181                included_service_path.push(parts[1]);
182
183                args.extend(Self::slurp_args(included_service_path)?);
184
185                continue;
186            }
187
188            let parts: Vec<&str> = line.split_whitespace().collect();
189            if parts.len() < 3 {
190                warn!("expect at least 3 parts for a pam module config");
191                // likely a blank line
192                continue;
193            }
194
195            let module = parts[2];
196            if module != "pam_motd.so" {
197                continue;
198            }
199            for arg in &parts[3..] {
200                if *arg != "noupdate" {
201                    args.push(String::from(*arg));
202                }
203            }
204        }
205
206        Ok(args)
207    }
208}
209
210/// Errors encountered while resolving the message of the day.
211#[non_exhaustive]
212#[derive(Debug)]
213pub enum Error {
214    /// An opaque error with a useful debugging message but
215    /// which callers should not dispatch on.
216    Err {
217        msg: String,
218    },
219    __NonExhaustive,
220}
221
222impl std::fmt::Display for Error {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
224        match self {
225            Error::Err { msg } => write!(f, "{}", msg)?,
226            _ => write!(f, "{:?}", self)?,
227        }
228
229        Ok(())
230    }
231}
232
233impl std::error::Error for Error {}