Add structopt option parsing, use a plain textfile for each user rather than YAML
This commit is contained in:
+109
-107
@@ -1,155 +1,157 @@
|
||||
use std::fs::{metadata, set_permissions};
|
||||
use std::io::{Error, ErrorKind, Read, Write};
|
||||
use std::fs::{metadata, set_permissions, File};
|
||||
use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, thread};
|
||||
use std::thread;
|
||||
use structopt::StructOpt;
|
||||
|
||||
extern crate yaml_rust;
|
||||
use crate::options::Opt;
|
||||
|
||||
use yaml_rust::{yaml::Hash, Yaml, YamlLoader};
|
||||
mod options;
|
||||
|
||||
const RESPONSE_NO: [u8; 4] = [0x0, 0x2, b'N', b'O'];
|
||||
const RESPONSE_OK: [u8; 4] = [0x0, 0x2, b'O', b'K'];
|
||||
|
||||
struct SaslSock {
|
||||
name: PathBuf,
|
||||
struct Listener {
|
||||
options: Opt,
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
impl SaslSock {
|
||||
pub fn new(name: &str) -> Result<SaslSock, Error> {
|
||||
let path = PathBuf::from(&name);
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)?;
|
||||
struct Server {
|
||||
options: Opt,
|
||||
stream: UnixStream,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
pub fn new(options: Opt) -> Result<Listener, Error> {
|
||||
if options.socket.exists() {
|
||||
std::fs::remove_file(&options.socket)?;
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(&name)?;
|
||||
let listener = UnixListener::bind(&options.socket)?;
|
||||
|
||||
let mut perms = metadata(&path)?.permissions();
|
||||
let mut perms = metadata(&options.socket)?.permissions();
|
||||
perms.set_mode(0o0777);
|
||||
set_permissions(&path, perms)?;
|
||||
set_permissions(&options.socket, perms)?;
|
||||
|
||||
Ok(SaslSock {
|
||||
name: PathBuf::from(&name),
|
||||
listener,
|
||||
})
|
||||
Ok(Listener { options, listener })
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<(), Error> {
|
||||
// accept connections and process them, spawning a new thread for each one
|
||||
for stream in self.listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
/* connection succeeded */
|
||||
let options = self.options.clone();
|
||||
thread::spawn(|| Server::new(options, stream).unwrap().handle_client());
|
||||
}
|
||||
Err(_err) => {
|
||||
/* connection failed */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SaslSock {
|
||||
impl Drop for Listener {
|
||||
fn drop(&mut self) {
|
||||
let _result = std::fs::remove_file(&self.name);
|
||||
let _result = std::fs::remove_file(&self.options.socket);
|
||||
println!("Shutting down");
|
||||
}
|
||||
}
|
||||
|
||||
/// The saslauthd protocol transmits strings as
|
||||
/// 16-bit unsigned network-byte-order lengths followed
|
||||
/// by the string itself.
|
||||
///
|
||||
fn read_string(stream: &mut UnixStream) -> Result<String, Error> {
|
||||
let mut length_buffer = [0u8, 0u8];
|
||||
|
||||
stream.read_exact(&mut length_buffer)?;
|
||||
let length = u16::from_be_bytes(length_buffer);
|
||||
|
||||
let mut string_buffer = vec![0u8; length as usize];
|
||||
|
||||
stream.read_exact(&mut string_buffer)?;
|
||||
|
||||
match String::from_utf8(string_buffer) {
|
||||
Ok(str) => Ok(str),
|
||||
Err(_err) => Err(Error::new(ErrorKind::Other, "Invalid UTF-8 sent")),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string(s: &str, stream: &mut UnixStream) -> Result<(), Error> {
|
||||
let length = s.len();
|
||||
|
||||
if length > 0xffff {
|
||||
return Err(Error::new(ErrorKind::Other, "String too long to write"));
|
||||
impl Server {
|
||||
pub fn new(options: Opt, stream: UnixStream) -> Result<Server, Error> {
|
||||
Ok(Server { options, stream })
|
||||
}
|
||||
|
||||
let length_buffer = [(length >> 8) as u8, (length & 0xff) as u8];
|
||||
/// The saslauthd protocol transmits strings as
|
||||
/// 16-bit unsigned network-byte-order lengths followed
|
||||
/// by the string itself.
|
||||
///
|
||||
fn read_string(&mut self) -> Result<String, Error> {
|
||||
let mut length_buffer = [0u8, 0u8];
|
||||
|
||||
stream.write_all(&length_buffer)?;
|
||||
stream.write_all(s.as_bytes())?;
|
||||
self.stream.read_exact(&mut length_buffer)?;
|
||||
let length = u16::from_be_bytes(length_buffer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let mut string_buffer = vec![0u8; length as usize];
|
||||
|
||||
fn check_auth(userid: &str, password: &str) -> Result<bool, Error> {
|
||||
let yaml = fs::read_to_string("test.yml")?;
|
||||
self.stream.read_exact(&mut string_buffer)?;
|
||||
|
||||
let docs =
|
||||
YamlLoader::load_from_str(&yaml)
|
||||
.map_err(|_| Error::new(ErrorKind::Other, "Can't parse YAML"))?;
|
||||
match String::from_utf8(string_buffer) {
|
||||
Ok(str) => Ok(str),
|
||||
Err(_err) => Err(Error::new(ErrorKind::Other, "Invalid UTF-8 sent")),
|
||||
}
|
||||
}
|
||||
|
||||
let doc = &docs[0];
|
||||
let users = doc["users"]
|
||||
.as_hash()
|
||||
.ok_or(Error::new(ErrorKind::Other, "Can't find 'users' in YAML"))?;
|
||||
fn write_string(&mut self, s: &str) -> Result<(), Error> {
|
||||
let length = s.len();
|
||||
|
||||
let passwords = users[&Yaml::from_str(&userid)].as_vec();
|
||||
if length > 0xffff {
|
||||
return Err(Error::new(ErrorKind::Other, "String too long to write"));
|
||||
}
|
||||
|
||||
if let Some(passwords) = passwords {
|
||||
// Found the passwords for the user, look for any matches
|
||||
let length_buffer = [(length >> 8) as u8, (length & 0xff) as u8];
|
||||
|
||||
for p in passwords {
|
||||
if let Some(str) = p.as_str() {
|
||||
if str == password {
|
||||
return Ok(true);
|
||||
}
|
||||
self.stream.write_all(&length_buffer)?;
|
||||
self.stream.write_all(s.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_auth(&mut self, userid: &str, password: &str) -> Result<bool, Error> {
|
||||
// Don't allow empty userids or passwords
|
||||
//
|
||||
if userid.is_empty() || password.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut password_file = self.options.passwords.clone();
|
||||
password_file.push(&userid);
|
||||
|
||||
let file = File::open(password_file)?;
|
||||
|
||||
for line in BufReader::new(file).lines().flatten() {
|
||||
if !line.starts_with('#') && line.trim() == password {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
fn communicate(&mut self) -> Result<(), Error> {
|
||||
let userid = self.read_string()?;
|
||||
let password = self.read_string()?;
|
||||
let service = self.read_string()?;
|
||||
let _realm = self.read_string()?;
|
||||
|
||||
|
||||
fn communicate(mut stream: UnixStream) -> Result<(), Error> {
|
||||
let userid = read_string(&mut stream)?;
|
||||
let password = read_string(&mut stream)?;
|
||||
let service = read_string(&mut stream)?;
|
||||
let _realm = read_string(&mut stream)?;
|
||||
|
||||
match check_auth(&userid, &password) {
|
||||
Ok(true) => stream.write_all(&RESPONSE_OK)?,
|
||||
Ok(false) => stream.write_all(&RESPONSE_NO)?,
|
||||
Err(e) => {
|
||||
stream.write_all(&RESPONSE_NO)?;
|
||||
stream.shutdown(Shutdown::Both)?;
|
||||
return Err(e)
|
||||
match self.check_auth(&userid, &password) {
|
||||
Ok(true) => self.stream.write_all(&RESPONSE_OK)?,
|
||||
Ok(false) => self.stream.write_all(&RESPONSE_NO)?,
|
||||
Err(e) => {
|
||||
self.stream.write_all(&RESPONSE_NO)?;
|
||||
self.stream.shutdown(Shutdown::Both)?;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_client(stream: UnixStream) {
|
||||
if let Err(error) = communicate(stream) {
|
||||
eprint!("{}", error);
|
||||
fn handle_client(&mut self) {
|
||||
if let Err(error) = self.communicate() {
|
||||
eprint!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let sock = SaslSock::new("/tmp/saslauthd.sock")?;
|
||||
|
||||
// accept connections and process them, spawning a new thread for each one
|
||||
for stream in sock.listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
/* connection succeeded */
|
||||
thread::spawn(|| handle_client(stream));
|
||||
}
|
||||
Err(_err) => {
|
||||
/* connection failed */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
let mut server = Listener::new(Opt::from_args())?;
|
||||
server.run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt, Clone, Debug)]
|
||||
#[structopt(name = "simple_saslauthd")]
|
||||
/// Rust-based web crawler
|
||||
pub struct Opt {
|
||||
#[structopt(
|
||||
short = "f",
|
||||
long = "socket",
|
||||
default_value = "/tmp/saslauthd.sock",
|
||||
parse(from_os_str)
|
||||
)]
|
||||
pub socket: PathBuf,
|
||||
|
||||
#[structopt(
|
||||
short = "p",
|
||||
long = "passwords",
|
||||
default_value = "./passwords",
|
||||
parse(from_os_str)
|
||||
)]
|
||||
pub passwords: PathBuf,
|
||||
}
|
||||
Reference in New Issue
Block a user