feat: clone from https://github.com/miquels/webdav-server-rs
This commit is contained in:
32
pam/Cargo.toml
Normal file
32
pam/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "pam-sandboxed"
|
||||
|
||||
# When releasing to crates.io:
|
||||
# - Update html_root_url in src/lib.rs
|
||||
# - Update CHANGELOG.md.
|
||||
# - Run: cargo readme > README.md
|
||||
# - Create git tag pam-sandboxed-0.x.y
|
||||
version = "0.2.0"
|
||||
|
||||
readme = "README.md"
|
||||
documentation = "https://docs.rs/pam-sandboxed"
|
||||
repository = "https://github.com/miquels/webdav-server-rs"
|
||||
homepage = "https://github.com/miquels/webdav-server-rs/tree/master/pam"
|
||||
authors = ["Miquel van Smoorenburg <mike@langeraar.net>"]
|
||||
edition = "2018"
|
||||
license = "Apache-2.0"
|
||||
categories = ["authentication"]
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.66"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.1"
|
||||
env_logger = "0.8.2"
|
||||
futures = "0.3.12"
|
||||
libc = "0.2.82"
|
||||
log = "0.4.13"
|
||||
serde = "1.0.120"
|
||||
serde_derive = "1.0.120"
|
||||
threadpool = "1.8.1"
|
||||
tokio = { version = "1.0.2", features = ["io-util", "net", "rt"] }
|
||||
49
pam/README.md
Normal file
49
pam/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
# pam-sandboxed
|
||||
|
||||
### PAM authentication with the pam library running in a separate process.
|
||||
|
||||
The PAM client in this crate creates a future that resolves with the
|
||||
PAM authentication result.
|
||||
|
||||
### HOW.
|
||||
|
||||
When initialized, the code fork()s and sets up a pipe-based communications
|
||||
channel between the parent (pam-client) and the child (pam-server). All
|
||||
the Pam work is then done on a threadpool in the child process.
|
||||
|
||||
### WHY.
|
||||
|
||||
Reasons for doing this instead of just calling libpam directly:
|
||||
|
||||
- Debian still comes with pam 1.8, which when calling setuid helpers
|
||||
will first close all filedescriptors up to the rlimit. if
|
||||
If that limit is high (millions) then it takes a looong while.
|
||||
`RLIMIT_NOFILE` is reset to a reasonably low number in the child process.
|
||||
- You might want to run the pam modules as a different user than
|
||||
the main process
|
||||
- There is code in libpam that might call setreuid(), and that is an
|
||||
absolute non-starter in threaded code.
|
||||
- Also, if you're mucking around with per-thread uid credentials on Linux by
|
||||
calling the setresuid syscall directly, the pthread library code that
|
||||
handles setuid() gets confused.
|
||||
|
||||
### EXAMPLE.
|
||||
```rust
|
||||
// call this once.
|
||||
let mut pam = PamAuth::new(None).expect("failed to initialized PAM");
|
||||
|
||||
// now use `pam` as a handle to authenticate.
|
||||
let fut = pam.auth("other", "user", "pass", None)
|
||||
.then(|res| {
|
||||
println!("pam auth result: {:?}", res);
|
||||
res
|
||||
});
|
||||
tokio::spawn(fut.map_err(|_| ()));
|
||||
```
|
||||
|
||||
### Copyright and License.
|
||||
|
||||
* © 2018, 2019 XS4ALL Internet bv
|
||||
* © 2018, 2019 Miquel van Smoorenburg
|
||||
* [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
11
pam/README.tpl
Normal file
11
pam/README.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
# {{crate}}
|
||||
|
||||
{{readme}}
|
||||
|
||||
### Copyright and License.
|
||||
|
||||
* © 2018, 2019 XS4ALL Internet bv
|
||||
* © 2018, 2019 Miquel van Smoorenburg
|
||||
* [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
11
pam/TODO.md
Normal file
11
pam/TODO.md
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
## PAM crate TODO items
|
||||
|
||||
- check all panics. If the server-side panics - that's OK, the client-side
|
||||
will just return errors.
|
||||
|
||||
- client side, when server has gone away:
|
||||
- panic ?
|
||||
- start returning errors ?
|
||||
- try to restart the server ?
|
||||
|
||||
6
pam/build.rs
Normal file
6
pam/build.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
extern crate cc;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-lib=pam");
|
||||
cc::Build::new().file("src/pam.c").compile("rpam"); // outputs `librpam.a`
|
||||
}
|
||||
92
pam/src/bin/main.rs
Normal file
92
pam/src/bin/main.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use env_logger;
|
||||
use pam_sandboxed::PamAuth;
|
||||
|
||||
fn prompt(s: &str) -> io::Result<String> {
|
||||
print!("{}", s);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
let name = prompt("What's your login? ")?;
|
||||
let pass = prompt("What's your password? ")?;
|
||||
|
||||
let mut pamauth = PamAuth::new(None)?;
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.build()?;
|
||||
|
||||
rt.block_on(async move {
|
||||
match pamauth.auth("other", &name, &pass, None).await {
|
||||
Ok(res) => println!("pam.auth returned Ok({:?})", res),
|
||||
Err(e) => println!("pam.auth returned error: {}", e),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// I've put the tests here in bin/main.rs instead of in lib.rs, because "cargo test"
|
||||
// for the library links the tests without -lpam, so it fails. The price we pay
|
||||
// for that is a dynamic test-mode setting in the library, instead of compile-time.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pam_sandboxed::{test_mode, PamAuth, PamError};
|
||||
use tokio;
|
||||
|
||||
const TEST_STR: &str = "xyzzy-test-test";
|
||||
|
||||
#[test]
|
||||
fn test_auth() {
|
||||
test_mode(true);
|
||||
|
||||
let mut pam = PamAuth::new(None).unwrap();
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let res = rt.block_on(async {
|
||||
let mut pam2 = pam.clone();
|
||||
|
||||
if let Err(e) = pam.auth(TEST_STR, "test", "foo", Some(TEST_STR)).await {
|
||||
eprintln!("auth(test) failed: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
if let Ok(_) = pam2.auth(TEST_STR, "unknown", "bar", Some(TEST_STR)).await {
|
||||
eprintln!("auth(unknown) succeeded, should have failed");
|
||||
return Err(PamError::unknown());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many() {
|
||||
test_mode(true);
|
||||
|
||||
let pam = PamAuth::new(None).unwrap();
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
rt.block_on(async move {
|
||||
for i in 1u32..=1000 {
|
||||
let mut pam = pam.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = pam.auth(TEST_STR, "test", "bar", Some(TEST_STR)).await {
|
||||
panic!("auth(test) failed at iteration {}: {:?}", i, e);
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
for handle in handles.drain(..) {
|
||||
let _ = handle.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
64
pam/src/lib.rs
Normal file
64
pam/src/lib.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
#![doc(html_root_url = "https://docs.rs/pam-sandboxed/0.2.0")]
|
||||
//! ## PAM authentication with the pam library running in a separate process.
|
||||
//!
|
||||
//! The PAM client in this crate creates a future that resolves with the
|
||||
//! PAM authentication result.
|
||||
//!
|
||||
//! ## HOW.
|
||||
//!
|
||||
//! When initialized, the code fork()s and sets up a pipe-based communications
|
||||
//! channel between the parent (pam-client) and the child (pam-server). All
|
||||
//! the Pam work is then done on a threadpool in the child process.
|
||||
//!
|
||||
//! ## WHY.
|
||||
//!
|
||||
//! Reasons for doing this instead of just calling libpam directly:
|
||||
//!
|
||||
//! - Debian still comes with pam 1.8, which when calling setuid helpers
|
||||
//! will first close all filedescriptors up to the rlimit. if
|
||||
//! If that limit is high (millions) then it takes a looong while.
|
||||
//! `RLIMIT_NOFILE` is reset to a reasonably low number in the child process.
|
||||
//! - You might want to run the pam modules as a different user than
|
||||
//! the main process
|
||||
//! - There is code in libpam that might call setreuid(), and that is an
|
||||
//! absolute non-starter in threaded code.
|
||||
//! - Also, if you're mucking around with per-thread uid credentials on Linux by
|
||||
//! calling the setresuid syscall directly, the pthread library code that
|
||||
//! handles setuid() gets confused.
|
||||
//!
|
||||
//! ## EXAMPLE.
|
||||
//! ```
|
||||
//! use pam_sandboxed::PamAuth;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! // call this once, early.
|
||||
//! let mut pam = PamAuth::new(None).expect("failed to initialized PAM");
|
||||
//!
|
||||
//! let mut rt = tokio::runtime::Runtime::new().expect("failed to initialize tokio runtime");
|
||||
//! rt.block_on(async move {
|
||||
//! let res = pam.auth("other", "user", "pass", None).await;
|
||||
//! println!("pam auth result: {:?}", res);
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
mod pam;
|
||||
mod pamclient;
|
||||
mod pamserver;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub use crate::pam::PamError;
|
||||
pub use crate::pamclient::PamAuth;
|
||||
|
||||
// See bin/main.rs, mod tests.
|
||||
#[doc(hidden)]
|
||||
pub fn test_mode(enabled: bool) {
|
||||
use crate::pam::TEST_MODE;
|
||||
let getal = if enabled { 1 } else { 0 };
|
||||
TEST_MODE.store(getal, Ordering::SeqCst);
|
||||
}
|
||||
82
pam/src/pam.c
Normal file
82
pam/src/pam.c
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <security/pam_appl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct creds {
|
||||
char *user;
|
||||
char *password;
|
||||
};
|
||||
|
||||
static void add_reply(struct pam_response **reply, int count, char *txt)
|
||||
{
|
||||
*reply = realloc(*reply, (count + 1) * sizeof(struct pam_response));
|
||||
(*reply)[count].resp_retcode = 0;
|
||||
(*reply)[count].resp = strdup(txt ? txt: "");
|
||||
}
|
||||
|
||||
static int c_pam_conv(int num_msg, const struct pam_message **msg,
|
||||
struct pam_response **resp, void *appdata)
|
||||
{
|
||||
struct pam_response *reply = NULL;
|
||||
struct creds *creds = (struct creds *)appdata;
|
||||
int replies = 0;
|
||||
|
||||
int count;
|
||||
for (count = 0; count < num_msg; count++) {
|
||||
switch (msg[count]->msg_style) {
|
||||
case PAM_PROMPT_ECHO_ON:
|
||||
add_reply(&reply, replies++, creds->user);
|
||||
break;
|
||||
case PAM_PROMPT_ECHO_OFF:
|
||||
add_reply(&reply, replies++, creds->password);
|
||||
break;
|
||||
case PAM_TEXT_INFO:
|
||||
break;
|
||||
case PAM_ERROR_MSG:
|
||||
default:
|
||||
if (reply != NULL)
|
||||
free(reply);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
}
|
||||
*resp = reply;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int c_pam_auth(char *service, char *user, char *pass, char *remip)
|
||||
{
|
||||
struct creds creds = {
|
||||
user,
|
||||
pass,
|
||||
};
|
||||
struct pam_conv conv = {
|
||||
c_pam_conv,
|
||||
&creds,
|
||||
};
|
||||
|
||||
pam_handle_t *pamh = NULL;
|
||||
int ret = pam_start(service, user, &conv, &pamh);
|
||||
if (ret != PAM_SUCCESS)
|
||||
return ret;
|
||||
if (ret == PAM_SUCCESS && remip && remip[0])
|
||||
ret = pam_set_item(pamh, PAM_RHOST, remip);
|
||||
if (ret == PAM_SUCCESS)
|
||||
ret = pam_authenticate(pamh, 0);
|
||||
pam_end(pamh, 0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void c_pam_lower_rlimits()
|
||||
{
|
||||
struct rlimit rlim;
|
||||
if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
|
||||
rlim_t l = rlim.rlim_cur;
|
||||
if (l > 256)
|
||||
l = 256;
|
||||
rlim.rlim_cur = l;
|
||||
rlim.rlim_max = l;
|
||||
setrlimit(RLIMIT_NOFILE, &rlim);
|
||||
}
|
||||
}
|
||||
97
pam/src/pam.rs
Normal file
97
pam/src/pam.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::error::Error;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
extern "C" {
|
||||
fn c_pam_auth(
|
||||
service: *const c_char,
|
||||
user: *const c_char,
|
||||
pass: *const c_char,
|
||||
remip: *const c_char,
|
||||
) -> c_int;
|
||||
fn _c_pam_return_value(index: c_int) -> c_int;
|
||||
fn pam_strerror(pamh: *const c_void, errnum: c_int) -> *const c_char;
|
||||
fn c_pam_lower_rlimits();
|
||||
}
|
||||
|
||||
pub(crate) const ERR_NUL_BYTE: i32 = 414243;
|
||||
pub(crate) const ERR_SEND_TO_SERVER: i32 = 414244;
|
||||
pub(crate) const ERR_RECV_FROM_SERVER: i32 = 414245;
|
||||
|
||||
pub(crate) static TEST_MODE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Error returned if authentication fails.
|
||||
///
|
||||
/// It's best not to try to interpret this, and handle all errors
|
||||
/// as "authentication failed".
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PamError(pub(crate) i32);
|
||||
|
||||
impl PamError {
|
||||
#[doc(hidden)]
|
||||
pub fn unknown() -> PamError {
|
||||
PamError(13)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PamError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
ERR_NUL_BYTE => write!(f, "embedded 0 byte in string"),
|
||||
ERR_SEND_TO_SERVER => write!(f, "error sending request to server"),
|
||||
ERR_RECV_FROM_SERVER => write!(f, "error receiving response from server"),
|
||||
_ => {
|
||||
let errnum = self.0 as c_int;
|
||||
let nullptr: *const c_void = std::ptr::null();
|
||||
let errstr = unsafe { CStr::from_ptr(pam_strerror(nullptr, errnum)).to_string_lossy() };
|
||||
f.write_str(&format!("PAM error: {}", errstr))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PamError {
|
||||
fn description(&self) -> &str {
|
||||
"PAM authentication error"
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ffi::NulError> for PamError {
|
||||
fn from(_e: std::ffi::NulError) -> Self {
|
||||
PamError(ERR_NUL_BYTE)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pam_auth(service: &str, user: &str, pass: &str, remip: &str) -> Result<(), PamError> {
|
||||
if TEST_MODE.load(Ordering::SeqCst) > 0 {
|
||||
return if user == "test" { Ok(()) } else { Err(PamError(1)) };
|
||||
}
|
||||
|
||||
let c_service = CString::new(service)?;
|
||||
let c_user = CString::new(user)?;
|
||||
let c_pass = CString::new(pass)?;
|
||||
let c_remip = CString::new(remip)?;
|
||||
let ret = unsafe {
|
||||
c_pam_auth(
|
||||
c_service.as_ptr(),
|
||||
c_user.as_ptr(),
|
||||
c_pass.as_ptr(),
|
||||
c_remip.as_ptr(),
|
||||
)
|
||||
};
|
||||
match ret {
|
||||
0 => Ok(()),
|
||||
errnum => Err(PamError(errnum)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pam_lower_rlimits() {
|
||||
unsafe {
|
||||
c_pam_lower_rlimits();
|
||||
}
|
||||
}
|
||||
265
pam/src/pamclient.rs
Normal file
265
pam/src/pamclient.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
// Client part, that is, the part that runs in the local process.
|
||||
//
|
||||
// All the futures based code lives here.
|
||||
//
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::sync::{Arc, Mutex, Once};
|
||||
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::join;
|
||||
use futures::{sink::SinkExt, stream::StreamExt};
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::unix::ReadHalf as UnixReadHalf;
|
||||
use tokio::net::unix::WriteHalf as UnixWriteHalf;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use crate::pam::{PamError, ERR_RECV_FROM_SERVER, ERR_SEND_TO_SERVER};
|
||||
use crate::pamserver::{PamResponse, PamServer};
|
||||
|
||||
// Request to be sent to the server process.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PamRequest {
|
||||
pub id: u64,
|
||||
pub user: String,
|
||||
pub pass: String,
|
||||
pub service: String,
|
||||
pub remip: Option<String>,
|
||||
}
|
||||
|
||||
// sent over request channel to PamAuthTask.
|
||||
struct PamRequest1 {
|
||||
req: PamRequest,
|
||||
resp_chan: oneshot::Sender<Result<(), PamError>>,
|
||||
}
|
||||
|
||||
/// Pam authenticator.
|
||||
#[derive(Clone)]
|
||||
pub struct PamAuth {
|
||||
inner: Arc<PamAuthInner>,
|
||||
}
|
||||
|
||||
struct PamAuthInner {
|
||||
once: Once,
|
||||
serversock: RefCell<Option<StdUnixStream>>,
|
||||
req_chan: RefCell<Option<mpsc::Sender<PamRequest1>>>,
|
||||
}
|
||||
|
||||
// Mutation of PamAuthInner only happens once,
|
||||
// protected by atomic Once, so this is safe.
|
||||
unsafe impl Sync for PamAuthInner {}
|
||||
unsafe impl Send for PamAuthInner {}
|
||||
|
||||
impl PamAuth {
|
||||
/// Create a new PAM authenticator. This will start a new PAM server process
|
||||
/// in the background, and it will contain a new PAM coordination task that
|
||||
/// will be lazily spawned the first time auth() is called.
|
||||
///
|
||||
/// Note that it is important to call this very early in main(), before any
|
||||
/// threads or runtimes have started.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pam_sandboxed::PamAuth;
|
||||
///
|
||||
/// fn main() -> Result<(), Box<std::error::Error>> {
|
||||
/// // get pam authentication handle.
|
||||
/// let mut pam = PamAuth::new(None)?;
|
||||
///
|
||||
/// // now start tokio runtime and use handle.
|
||||
/// let mut rt = tokio::runtime::Runtime::new()?;
|
||||
/// rt.block_on(async move {
|
||||
/// let res = pam.auth("other", "user", "pass", None).await;
|
||||
/// println!("pam auth result: {:?}", res);
|
||||
/// });
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
pub fn new(num_threads: Option<usize>) -> Result<PamAuth, io::Error> {
|
||||
// spawn the server process.
|
||||
let serversock = PamServer::start(num_threads)?;
|
||||
|
||||
let inner = PamAuthInner {
|
||||
once: Once::new(),
|
||||
req_chan: RefCell::new(None),
|
||||
serversock: RefCell::new(Some(serversock)),
|
||||
};
|
||||
Ok(PamAuth {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
/// Authenticate via pam and return the result.
|
||||
///
|
||||
/// - `service`: PAM service to use - usually "other".
|
||||
/// - `username`: account username
|
||||
/// - `password`: account password
|
||||
/// - `remoteip`: if this is a networking service, the remote IP address of the client.
|
||||
pub async fn auth(
|
||||
&mut self,
|
||||
service: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
remoteip: Option<&str>,
|
||||
) -> Result<(), PamError>
|
||||
{
|
||||
// If we haven't started the background task yet, do it now.
|
||||
// That also initializes req_chan.
|
||||
let inner = &self.inner;
|
||||
inner.once.call_once(|| {
|
||||
// These should not ever panic on unwrap().
|
||||
let serversock = inner.serversock.borrow_mut().take().unwrap();
|
||||
inner
|
||||
.req_chan
|
||||
.replace(Some(PamAuthTask::start(serversock).unwrap()));
|
||||
});
|
||||
|
||||
// create request to be sent to the server.
|
||||
let req = PamRequest {
|
||||
id: 0,
|
||||
user: username.to_string(),
|
||||
pass: password.to_string(),
|
||||
service: service.to_string(),
|
||||
remip: remoteip.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
// add a one-shot channel for the response.
|
||||
let (tx, rx) = oneshot::channel::<Result<(), PamError>>();
|
||||
|
||||
// put it all together and send it.
|
||||
let req1 = PamRequest1 {
|
||||
req: req,
|
||||
resp_chan: tx,
|
||||
};
|
||||
let mut authtask_chan = inner.req_chan.borrow().as_ref().unwrap().clone();
|
||||
authtask_chan
|
||||
.send(req1)
|
||||
.await
|
||||
.map_err(|_| PamError(ERR_SEND_TO_SERVER))?;
|
||||
|
||||
// wait for the response.
|
||||
match rx.await {
|
||||
Ok(res) => res,
|
||||
Err(_) => Err(PamError(ERR_RECV_FROM_SERVER)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared data for the PamAuthTask tasks.
|
||||
struct PamAuthTask {
|
||||
// clients waiting for a response.
|
||||
waiters: Mutex<HashMap<u64, oneshot::Sender<Result<(), PamError>>>>,
|
||||
}
|
||||
|
||||
impl PamAuthTask {
|
||||
// Start the server process. Then return a handle to send requests on.
|
||||
fn start(serversock: StdUnixStream) -> io::Result<mpsc::Sender<PamRequest1>> {
|
||||
let mut serversock = UnixStream::from_std(serversock)?;
|
||||
|
||||
// create a request channel.
|
||||
let (req_tx, req_rx) = mpsc::channel::<PamRequest1>(0);
|
||||
|
||||
// shared state between request and response task.
|
||||
let this = PamAuthTask {
|
||||
waiters: Mutex::new(HashMap::new()),
|
||||
};
|
||||
|
||||
debug!("PamAuthTask: spawning task on runtime");
|
||||
tokio::spawn(async move {
|
||||
// split serversock into send/receive halves.
|
||||
let (srx, stx) = serversock.split();
|
||||
|
||||
join!(this.handle_request(req_rx, stx), this.handle_response(srx));
|
||||
});
|
||||
|
||||
Ok(req_tx)
|
||||
}
|
||||
|
||||
async fn handle_request(&self, mut req_rx: mpsc::Receiver<PamRequest1>, mut stx: UnixWriteHalf<'_>) {
|
||||
let mut id: u64 = 0;
|
||||
loop {
|
||||
// receive next request.
|
||||
let PamRequest1 { mut req, resp_chan } = match req_rx.next().await {
|
||||
Some(r1) => r1,
|
||||
None => {
|
||||
// PamAuth handle was dropped. Ask server to exit.
|
||||
let data = [0u8; 2];
|
||||
let _ = stx.write_all(&data).await;
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
// store the response channel.
|
||||
req.id = id;
|
||||
id += 1;
|
||||
{
|
||||
let mut waiters = self.waiters.lock().unwrap();
|
||||
waiters.insert(req.id, resp_chan);
|
||||
}
|
||||
|
||||
// serialize data and send.
|
||||
let mut data: Vec<u8> = match bincode::serialize(&req) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
// this panic can never happen at runtime.
|
||||
panic!("PamClient: serializing data: {:?}", e);
|
||||
},
|
||||
};
|
||||
if data.len() > 65533 {
|
||||
// this panic can never happen at runtime.
|
||||
panic!("PamClient: serialized data > 65533 bytes");
|
||||
}
|
||||
let l1 = ((data.len() >> 8) & 0xff) as u8;
|
||||
let l2 = (data.len() & 0xff) as u8;
|
||||
data.insert(0, l1);
|
||||
data.insert(1, l2);
|
||||
if let Err(e) = stx.write_all(&data).await {
|
||||
// this can happen if the server has gone away.
|
||||
// in which case, handle_response() will exit as well.
|
||||
error!("PamClient: FATAL: writing data to server: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_response(&self, mut srx: UnixReadHalf<'_>) {
|
||||
loop {
|
||||
// read size header.
|
||||
let mut buf = [0u8; 2];
|
||||
if let Err(_) = srx.read_exact(&mut buf).await {
|
||||
error!("PamClient: FATAL: short read, server gone away?!");
|
||||
return;
|
||||
}
|
||||
let sz = ((buf[0] as usize) << 8) + (buf[1] as usize);
|
||||
|
||||
// read response data.
|
||||
let mut data = Vec::with_capacity(sz);
|
||||
data.resize(sz, 0u8);
|
||||
if let Err(_) = srx.read_exact(&mut data[..]).await {
|
||||
error!("PamClient: FATAL: short read, server gone away?!");
|
||||
return;
|
||||
}
|
||||
|
||||
// deserialize.
|
||||
let resp: PamResponse = match bincode::deserialize(&data[..]) {
|
||||
Ok(req) => req,
|
||||
Err(_) => {
|
||||
// this panic can never happen at runtime.
|
||||
panic!("PamCLient: error deserializing response");
|
||||
},
|
||||
};
|
||||
|
||||
// and send response to waiting requester.
|
||||
let resp_chan = {
|
||||
let mut waiters = self.waiters.lock().unwrap();
|
||||
waiters.remove(&resp.id)
|
||||
};
|
||||
if let Some(resp_chan) = resp_chan {
|
||||
let _ = resp_chan.send(resp.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
pam/src/pamserver.rs
Normal file
169
pam/src/pamserver.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
// Server part - the code here is fork()ed off and lives in its own
|
||||
// process. We communicate with it through a unix stream socket.
|
||||
//
|
||||
// This is all old-fashioned blocking and thread-based code.
|
||||
//
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use bincode::{deserialize, serialize};
|
||||
use libc;
|
||||
|
||||
use crate::pam::{pam_auth, pam_lower_rlimits, PamError};
|
||||
use crate::pamclient::PamRequest;
|
||||
|
||||
// Response back from the server process.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PamResponse {
|
||||
pub id: u64,
|
||||
pub result: Result<(), PamError>,
|
||||
}
|
||||
|
||||
// server side.
|
||||
pub(crate) struct PamServer {
|
||||
rx_socket: StdUnixStream,
|
||||
tx_socket: Arc<Mutex<StdUnixStream>>,
|
||||
}
|
||||
|
||||
impl PamServer {
|
||||
// fork and start the server, return the stream socket for communication.
|
||||
pub(crate) fn start(num_threads: Option<usize>) -> Result<StdUnixStream, io::Error> {
|
||||
// Create a unix socketpair for communication.
|
||||
let (sock1, sock2) = StdUnixStream::pair()?;
|
||||
let sock3 = sock2.try_clone()?;
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
// fork server.
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if pid == 0 {
|
||||
// first, close all filedescriptors (well, all..)
|
||||
for fdno in 3..8192 {
|
||||
if fdno != sock2.as_raw_fd() && fdno != sock3.as_raw_fd() {
|
||||
unsafe {
|
||||
libc::close(fdno);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut server = PamServer {
|
||||
rx_socket: sock2,
|
||||
tx_socket: Arc::new(Mutex::new(sock3)),
|
||||
};
|
||||
pam_lower_rlimits();
|
||||
trace!("PamServer: child: starting server");
|
||||
server.serve(num_threads.unwrap_or(8));
|
||||
drop(server);
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
handle.join().unwrap()?;
|
||||
|
||||
trace!("PamServer: parent: started server");
|
||||
Ok(sock1)
|
||||
}
|
||||
|
||||
// serve requests.
|
||||
fn serve(&mut self, num_threads: usize) {
|
||||
// create a threadpool, then serve connections via the threadpool.
|
||||
let pool = threadpool::ThreadPool::new(num_threads);
|
||||
|
||||
// process incoming connections.
|
||||
loop {
|
||||
// read length.
|
||||
let mut buf = [0u8; 2];
|
||||
let res = self.rx_socket.read_exact(&mut buf);
|
||||
if let Err(e) = res {
|
||||
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||
// parent probably exited - not an error.
|
||||
trace!("PamServer::serve: EOF reached on input");
|
||||
break;
|
||||
}
|
||||
panic!("PamServer::serve: read socket: {}", e);
|
||||
}
|
||||
let sz = ((buf[0] as usize) << 8) + (buf[1] as usize);
|
||||
if sz == 0 {
|
||||
// size 0 packet indicates client wants to shut us down.
|
||||
trace!("PamServer::serve: EOF packet on input");
|
||||
break;
|
||||
}
|
||||
|
||||
// read request data.
|
||||
let mut data = Vec::with_capacity(sz);
|
||||
data.resize(sz, 0u8);
|
||||
let res = self.rx_socket.read_exact(&mut data);
|
||||
if let Err(e) = res {
|
||||
panic!("PamServer::serve: read socket: {}", e);
|
||||
}
|
||||
let req: PamRequest = match deserialize(&data[..]) {
|
||||
Ok(req) => req,
|
||||
Err(_) => panic!("PamServer::serve: error deserializing request"),
|
||||
};
|
||||
trace!(
|
||||
"PamServer::serve: read request {:?} active threads: {} queued {}",
|
||||
req,
|
||||
pool.active_count(),
|
||||
pool.queued_count()
|
||||
);
|
||||
|
||||
// run request on pool.
|
||||
let sock = self.tx_socket.clone();
|
||||
pool.execute(move || {
|
||||
if let Err(e) = pam_process(req, sock) {
|
||||
panic!("PamServer::pam_process: error: {}", e);
|
||||
}
|
||||
});
|
||||
let mut i = 0;
|
||||
while pool.queued_count() > 2 * pool.max_count() {
|
||||
if i == 399 {
|
||||
debug!(
|
||||
"PamServer::serve: pool busy! active {}, max {}, queued: {}",
|
||||
pool.active_count(),
|
||||
pool.max_count(),
|
||||
pool.queued_count()
|
||||
);
|
||||
}
|
||||
i += 1;
|
||||
i = i % 400;
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
pool.join();
|
||||
trace!("PamServer::serve: exit.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Process one request. This is run on the threadpool.
|
||||
fn pam_process(req: PamRequest, sock: Arc<Mutex<StdUnixStream>>) -> Result<(), io::Error> {
|
||||
trace!("PamServer::pam_process: starting with request {:?}", req);
|
||||
|
||||
// authenticate.
|
||||
let remip = req.remip.as_ref().map(|s| s.as_str()).unwrap_or("");
|
||||
let res = PamResponse {
|
||||
id: req.id,
|
||||
result: pam_auth(&req.service, &req.user, &req.pass, remip),
|
||||
};
|
||||
|
||||
// and send back result.
|
||||
trace!("PamServer::pam_process: returning response {:?}", res);
|
||||
let mut response: Vec<u8> = serialize(&res)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("error serializing response: {}", e)))?;
|
||||
let l1 = ((response.len() >> 8) & 0xff) as u8;
|
||||
let l2 = (response.len() & 0xff) as u8;
|
||||
response.insert(0, l1);
|
||||
response.insert(1, l2);
|
||||
|
||||
match sock.lock().unwrap().write_all(&response) {
|
||||
Err(e) => {
|
||||
debug!("PamServer::pam_process: writing to response socket: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
Ok(..) => Ok(()),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user