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 {}