diff options
author | Yves Fischer <yvesf-git@xapek.org> | 2016-12-17 15:50:27 +0100 |
---|---|---|
committer | Yves Fischer <yvesf-git@xapek.org> | 2016-12-17 16:00:19 +0100 |
commit | 8ceac0236c555979a9172c69c1a9e895d7487d6f (patch) | |
tree | 390740422d514ac62bc572d61c64e7375f2d7a27 /rust/src | |
parent | f355162fde31a4c79b7e665b43cef2de3ba05cb5 (diff) | |
download | auth-xmppmessage-8ceac0236c555979a9172c69c1a9e895d7487d6f.tar.gz auth-xmppmessage-8ceac0236c555979a9172c69c1a9e895d7487d6f.zip |
replace all with subdirectory rust/
Diffstat (limited to 'rust/src')
-rw-r--r-- | rust/src/apachelog.rs | 45 | ||||
-rw-r--r-- | rust/src/handler.rs | 348 | ||||
-rw-r--r-- | rust/src/main.rs | 98 | ||||
-rw-r--r-- | rust/src/message.rs | 22 | ||||
-rw-r--r-- | rust/src/sendxmpp.rs | 21 | ||||
-rw-r--r-- | rust/src/token.rs | 73 |
6 files changed, 0 insertions, 607 deletions
diff --git a/rust/src/apachelog.rs b/rust/src/apachelog.rs deleted file mode 100644 index fb5d1a6..0000000 --- a/rust/src/apachelog.rs +++ /dev/null @@ -1,45 +0,0 @@ -///! Prints logging similar to apache http access.log -use std::net::IpAddr; -use std::io::Read; - -use time; -use tiny_http::{Request, Response}; - -pub struct LogEntry { - remote_ip_address: IpAddr, - remote_user: String, - request_path: String, - time: time::Tm, - status: u16, - response_size: u32, -} - -impl LogEntry { - pub fn start(req: &Request) -> LogEntry { - let entry = LogEntry { - remote_ip_address: req.remote_addr().ip(), - remote_user: String::new(), - request_path: String::from(req.url()), - time: time::now(), - status: 0, - response_size: 0, - }; - return entry - } - - pub fn done<R>(&mut self, _: &Response<R>, status_code: u16) where R: Read { - self.status = status_code; // request.statuscode is not accessible :( - self.print(); - } - - #[inline(always)] - fn print(&self) { - info!("{} - {} - [{}] \"{}\" {} {}", - self.remote_ip_address, - self.remote_user, - time::strftime("%d/%b/%Y %T %z", &self.time).unwrap(), - self.request_path, - self.status, - self.response_size); - } -} diff --git a/rust/src/handler.rs b/rust/src/handler.rs deleted file mode 100644 index 97f5d5c..0000000 --- a/rust/src/handler.rs +++ /dev/null @@ -1,348 +0,0 @@ -use std::cell::Cell; -use std::collections::{HashMap}; -use std::io; -use std::marker::Sync; -use std::ops::Add; -use std::str; -use std::str::FromStr; -use std::sync::{Arc, RwLock}; -use std::thread; -use std::time::Duration as StdDuration; - -use apachelog::LogEntry; -use sendxmpp; -use time::{get_time, Duration as TimeDuration}; -use token; -use message::format_message; - -use ascii::AsciiString; -use tiny_http::{Request, Response, StatusCode, Header, HeaderField}; -use rustc_serialize::base64::{FromBase64}; - -pub struct HeaderInfos { - auth_username: String, - auth_password: String, - auth_method: String, - allowed_jids: Vec<String> -} - -pub struct AuthHandler { - bot_jid: String, - bot_password: String, - valid_tokens_cache: Arc<RwLock<HashMap<String, i64>>>, - tg: token::TokenGenerator, - last_interactive_request: Cell<i64>, - nosend: bool, - authenticate_header: Header -} - -type EmptyResponse = Response<io::Empty>; - -// HTTP Statuscodes defined as macro. This way they can be used like literals. -macro_rules! http_header_authorization { () => (r"Authorization") } -macro_rules! http_header_x_allowed_jid { () => (r"X-Allowed-Jid") } -macro_rules! http_header_www_authenticate { () => (r"WWW-Authenticate") } - -// Finds a header in a `tiny_http::Header` structure. -macro_rules! get_header { - ($headers:expr, $name:expr) => ($headers.iter() - .filter(|h| h.field.equiv($name)) - .next().ok_or(concat!("No Header found named: '", $name, "'"))); -} - -impl AuthHandler { - pub fn make(bot_jid: String, bot_password: String, validity: TimeDuration, - secret: Vec<u8>, nosend: bool) -> AuthHandler { - return AuthHandler { - bot_jid: bot_jid, - bot_password: bot_password, - valid_tokens_cache: Arc::new(RwLock::new(HashMap::new())), - tg: token::TokenGenerator::new(validity.num_seconds(), secret), - last_interactive_request: Cell::new(0), - nosend: nosend, - authenticate_header: Header { - field: HeaderField::from_bytes(http_header_www_authenticate!()).unwrap(), - value: AsciiString::from_str(r#"Basic realm="xmppmessage auth""#).unwrap() - } - } - } - - fn send_message(&self, user_jid: &str) { - let (valid_from, valid_until, token) = self.tg.generate_token(user_jid, get_time().sec); - let message = format_message(token, valid_from, valid_until); - if self.nosend { - error!("Would send to {} message: {}", user_jid, message); - } else { - if sendxmpp::send_message(self.bot_jid.as_str(), self.bot_password.as_str(), - message.as_str(), user_jid).is_err() { - error!("Failed to send message"); - } - } - } - - #[inline(always)] - fn parse_headers(headers: &[Header]) -> Result<HeaderInfos, &'static str> { - let auth_header = get_header!(headers, http_header_authorization!())?.value.as_str(); - debug!("{}: {}", http_header_authorization!(), auth_header); - let (auth_method, encoded_cred) = match auth_header.find(' ') { - Some(pos) => Ok((auth_header, pos)), - None => Err("Failed to split Authorization header") - }.map(|(header, pos)| header.split_at(pos))?; - - let decoded_cred = encoded_cred.trim().from_base64() - .or(Err("Failed to decode base64 of username/password"))?; - - let (username, password) = str::from_utf8(&decoded_cred) - .or(Err("Failed to decode UTF-8 of username/password")) - .map(|value| match value.find(':') { - Some(pos) => Ok((value, pos)), - None => Err("Failed to split username/password") - })? - .map(|(value, pos)| value.split_at(pos)) - .map(|(username, colon_password)| (username, colon_password.split_at(1).1))?; - - let allowed_jids_header = get_header!(headers, http_header_x_allowed_jid!())?.value.as_str(); - debug!("{}: {}", http_header_x_allowed_jid!(), allowed_jids_header); - let allowed_jids_list = allowed_jids_header.split(',').map(String::from).collect(); - - Ok(HeaderInfos { - auth_username: String::from(username), - auth_password: String::from(password), - auth_method: String::from(auth_method), - allowed_jids: allowed_jids_list, - }) - } - - fn authenticate_response(&self, status_code: u16) -> io::Result<(u16, EmptyResponse)> { - Ok((status_code, Response::new( - StatusCode(status_code), - vec![self.authenticate_header.clone()], - io::empty(), None, None - ))) - } - - fn _call_internal(&self, request: &Request) -> io::Result<(u16, EmptyResponse)> { - let current_time = get_time().sec; - return match AuthHandler::parse_headers(request.headers()) { - Ok(headerinfos) => { - let is_known_user = headerinfos.allowed_jids.contains(&headerinfos.auth_username); - if headerinfos.auth_method != "Basic" { - error!("Invalid authentication method"); - return self.authenticate_response(405) // Method not allowed - } else if headerinfos.auth_username.len() > 0 && headerinfos.auth_password.len() == 0 { - // Request new token - if current_time - self.last_interactive_request.get() < 2 { - // If last error was not longer then 2 second ago then sleep - info!("Too many invalid token-requests, sleep 5 seconds"); - thread::sleep(StdDuration::from_secs(5)); - return self.authenticate_response(429) // Too many requests - } else { - self.last_interactive_request.set(current_time); - if is_known_user { - self.send_message(&headerinfos.auth_username); - } - return self.authenticate_response(401) //Token sent, retry now - } - } else { - match self.verify(&headerinfos) { - Ok(true) => { - if is_known_user { - return Ok((200, Response::empty(200))) // Ok - } else { - self.last_interactive_request.set(current_time); - return self.authenticate_response(401) // invalid password - } - }, - Ok(false) => { - if current_time - self.last_interactive_request.get() < 2 { - // If last error was not longer then 2 seconds ago then sleep 5 seconds - thread::sleep(StdDuration::from_secs(5)); - return Ok((428, Response::empty(429))) // Too Many Requests - } else { - self.last_interactive_request.set(current_time); - // in this case we use the chance to delete outdated cache entries - match self.clean_cache() { - Ok(num) => debug!("Removed {} cache entries", num), - Err(e) => error!("{}", e), - }; - return self.authenticate_response(401) // Authentication failed, username or password wrong - } - }, - Err(msg) => { - error!("verify failed: {}", msg); - return Err(io::Error::new(io::ErrorKind::Other, "Server Error")) // Server Error - } - } - } - }, - Err(e) => { - info!("Error: {}", e); - return self.authenticate_response(401) // No Authorization header - }, - }; - } - - fn clean_cache(&self) -> Result<usize, &'static str> { - let now = get_time().sec; - let guard = self.valid_tokens_cache.clone(); - let mut cache = try!(guard.write().or(Err("Failed to get write lock on cache"))); - let outdated_keys = cache.iter().filter(|&(_, &v)| v < now).map(|(k, _)| k.clone()) - .collect::<Vec<_>>(); - let num = outdated_keys.iter().map(move |key| cache.remove(key)).count(); - Ok(num) - } - - fn verify(&self, headerinfos: &HeaderInfos) -> Result<bool, &'static str> { - let pw_token = token::normalize_token(&headerinfos.auth_password); - let guard = self.valid_tokens_cache.clone(); - let key = headerinfos.auth_username.clone().add(":").add(pw_token.as_str()); - let current_time = get_time().sec; - - // try cache: - let result1 = { - let read_cache = try!(guard.read().or(Err("Failed to read-lock cache"))); - read_cache.get(&key).ok_or(()).and_then({ - |valid_until| - if valid_until > ¤t_time { - Ok(true) - } else { - Err(()) // Value in cache but expired - } - }) - }; - // or compute and compare, eventually store it in cache - match result1 { - Ok(true) => Ok(true), - _ => { - let t1 = get_time().sec - self.tg.valid_duration_secs; - let (valid_from1, valid_until1, token1) = self.tg.generate_token_norm(&headerinfos.auth_username, t1); - if pw_token == token1 { - let mut cache = try!(guard.write().or(Err("Failed to get write lock on cache"))); - debug!("Cache for {} from {} until {}", headerinfos.auth_username, valid_from1, valid_until1); - cache.insert(key, valid_until1); - return Ok(true) - } else { - let t2 = get_time().sec; - let (valid_from2, valid_until2, token2) = self.tg.generate_token_norm(&headerinfos.auth_username, t2); - if pw_token == token2 { - let mut cache = try!(guard.write().or(Err("Failed to get write lock on cache"))); - debug!("Cache for {} from {} until {}", headerinfos.auth_username, valid_from2, valid_until2); - cache.insert(key, valid_until2); - return Ok(true) - } - } - warn!("Invalid token for {}", headerinfos.auth_username); - Ok(false) - } - } - } - - #[inline(always)] - pub fn call(&self, request: &Request) -> Response<io::Empty> { - let mut log = LogEntry::start(&request); - let (status_code, response) = self._call_internal(request).unwrap_or_else(|err: io::Error| { - error!("{}", err); - (500, Response::empty(500)) - }); - log.done(&response, status_code); - - return response; - } -} - -unsafe impl Sync for AuthHandler {} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - use time::{Duration as TimeDuration}; - use ascii::AsciiString; - use tiny_http::{Header, HeaderField}; - use rustc_serialize::base64::{MIME, ToBase64}; - - macro_rules! assert_error_starts_with { - ($result:expr, $pattern:expr) => {{ - assert!($result.is_err(), "Must be error"); - let msg = $result.err().unwrap(); - assert!(msg.starts_with($pattern), - "Error message '{}' does not start with '{}", - msg, $pattern); - }} - } - - macro_rules! assert_is_ok { - ($result:expr) => (assert!($result.is_ok(), "Result not is_ok(): {}", $result.err().unwrap())); - } - - - #[test] - fn test_handler_creation() { - let handler = AuthHandler::make("jid".to_string(), "pw".to_string(), - TimeDuration::hours(123), - vec!(1, 2, 3), - true); - assert_eq!(handler.bot_jid, "jid"); - assert_eq!(handler.bot_password, "pw"); - assert_eq!(handler.tg.valid_duration_secs, 60 * 60 * 123); - assert_eq!(handler.nosend, true); - } - - #[test] - fn test_parse_headers1() { - let result = AuthHandler::parse_headers(&[Header { - field: HeaderField::from_bytes(http_header_authorization!()).unwrap(), - value: AsciiString::from_str(r#"adsasdasd"#).unwrap() - }]); - assert_error_starts_with!(result, "Failed to split Authorization header"); - } - - #[test] - fn test_parse_headers2() { - let result = AuthHandler::parse_headers(&[Header { - field: HeaderField::from_bytes(http_header_authorization!()).unwrap(), - value: AsciiString::from_str("adsasdasd AB$$").unwrap() - }]); - assert_error_starts_with!(result, "Failed to decode base64"); - } - - #[test] - fn test_parse_headers3() { - let header_value = String::from("methodname ") + &(b"adfasdasd".to_base64(MIME)); - let result = AuthHandler::parse_headers(&[Header { - field: HeaderField::from_bytes(http_header_authorization!()).unwrap(), - value: AsciiString::from_str(&header_value).unwrap() - }]); - assert_error_starts_with!(result, "Failed to split username"); - } - - #[test] - fn test_parse_headers4() { - let header_value = String::from("methodname ") + &(b"adfasdasd:asdfasd".to_base64(MIME)); - let result = AuthHandler::parse_headers(&[Header { - field: HeaderField::from_bytes(http_header_authorization!()).unwrap(), - value: AsciiString::from_str(&header_value).unwrap() - }]); - assert_error_starts_with!(result, "No Header found named: 'X-A"); - } - - #[test] - fn test_parse_headers5() { - let header_value = String::from("methodname ") + &(b"adfasdasd:password".to_base64(MIME)); - let result = AuthHandler::parse_headers(&[Header { - field: HeaderField::from_bytes(http_header_authorization!()).unwrap(), - value: AsciiString::from_str(&header_value).unwrap() - }, Header { - field: HeaderField::from_bytes(http_header_x_allowed_jid!()).unwrap(), - value: AsciiString::from_str("foo@bar,bla@bla.com").unwrap() - }]); - assert_is_ok!(result); - let headerinfos = result.unwrap(); - assert_eq!(vec!["foo@bar", "bla@bla.com"], headerinfos.allowed_jids); - assert_eq!("adfasdasd", headerinfos.auth_username); - assert_eq!("password", headerinfos.auth_password); - assert_eq!("methodname", headerinfos.auth_method); - } -}
\ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs deleted file mode 100644 index 30d4f8b..0000000 --- a/rust/src/main.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::env; -use std::iter::repeat; -use std::sync::Arc; -use std::thread; - -extern crate ascii; -extern crate crypto; -extern crate getopts; -#[macro_use] extern crate log; -extern crate tiny_http; -extern crate time; -extern crate rand; -extern crate rustc_serialize; -extern crate simple_logger; - -use crypto::digest::Digest; -use crypto::sha1::Sha1; -use getopts::Options; -use rand::{thread_rng, Rng}; -use log::LogLevel; - -mod apachelog; -mod handler; -mod message; -mod sendxmpp; -mod token; - -fn print_usage(program: &str, opts: Options) { - let brief = format!("Usage: {} [options]", program); - print!("{}", opts.usage(&brief)); -} - -fn main() { - let args: Vec<String> = env::args().collect(); - let program = args[0].clone(); - let mut opts = Options::new(); - opts.optopt("j", "jid", "bot jid", "JID"); - opts.optopt("p", "password", "bot password", "PASSWORD"); - opts.optopt("s", "secret", "server secret for token generation", "SECRET"); - opts.optopt("t", "time", "Validity of the token in hours (default 48)", "HOURS"); - opts.optopt("o", "port", "TCP Port to listen on", "PORT"); - opts.optflag("d", "debug", "Use loglevel Debug instead of Warn"); - opts.optflag("n", "nosend", "Don't send XMPP message, just print debug infos"); - opts.optflag("h", "help", "print this help menu"); - let matches = opts.parse(&args[1..]).unwrap_or_else(|f| panic!(f.to_string())); - - if matches.opt_present("h") { - print_usage(&program, opts); - return; - } - - if !(matches.opt_present("j") && matches.opt_present("p")) && !matches.opt_present("n") { - print_usage(&program, opts); - panic!("Missing jid or password"); - } - - simple_logger::init_with_level(if matches.opt_present("d") { LogLevel::Debug } else { LogLevel::Warn }).unwrap(); - - let mut hasher = Sha1::new(); - let mut secret: Vec<u8> = repeat(0).take((hasher.output_bits() + 7) / 8).collect(); - matches.opt_str("s").and_then(|s| { - hasher.input_str(s.as_str()); - hasher.result(&mut secret); - Some(()) - }).unwrap_or_else(|| { - println!("No secret (-s/--secret) given, using random value"); - thread_rng().fill_bytes(&mut secret); - }); - let secret = secret.into_iter().take(16).collect::<Vec<u8>>(); - let validity: i64 = matches.opt_str("t").unwrap_or(String::from("48")).parse() - .unwrap_or_else(|_| { panic!("Failed to parse time") }); - let port = matches.opt_str("o").unwrap_or(String::from("8080")).parse() - .unwrap_or_else(|_| { panic!("Failed to parse port number") }); - - let handler = handler::AuthHandler::make(matches.opt_str("j").unwrap_or(String::from("<jid>")), - matches.opt_str("p").unwrap_or(String::from("<password>")), - time::Duration::hours(validity), - secret, - matches.opt_present("n")); - let handler = Arc::new(handler); - let server = Arc::new(tiny_http::Server::http(("0.0.0.0", port)).unwrap()); - - let mut threads = Vec::new(); - - for _ in 0..2 { - let server = server.clone(); - let handler = handler.clone(); - threads.push(thread::spawn(move || { - for request in server.incoming_requests() { - let response = handler.call(&request); - let _ = request.respond(response); - } - })); - } - for h in threads { - h.join().unwrap(); - } -}
\ No newline at end of file diff --git a/rust/src/message.rs b/rust/src/message.rs deleted file mode 100644 index 0af1a13..0000000 --- a/rust/src/message.rs +++ /dev/null @@ -1,22 +0,0 @@ -///! Formats the message to be sent to the user -use time::{at_utc, Timespec, strftime}; - - -pub fn format_message(token: String, valid_from: i64, valid_until: i64) -> String { - return format!("Token: {}. Valid from {} until {}", - token, - strftime("%F %X", &at_utc(Timespec::new(valid_from, 0))).unwrap(), - strftime("%F %X", &at_utc(Timespec::new(valid_until, 0))).unwrap()); -} - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test1() { - assert_eq!(format_message("7A-74-F4".to_string(), 0, 1481831953), - "Token: 7A-74-F4. Valid from 1970-01-01 00:00:00 until 2016-12-15 19:59:13"); - } -}
\ No newline at end of file diff --git a/rust/src/sendxmpp.rs b/rust/src/sendxmpp.rs deleted file mode 100644 index 2db471e..0000000 --- a/rust/src/sendxmpp.rs +++ /dev/null @@ -1,21 +0,0 @@ -///! Interface for the C implementation oof sending xmpp message - -use std::ffi::{CString, NulError}; -use std::os::raw::c_char; - -pub fn send_message(jid: &str, password: &str, message: &str, to: &str) -> Result<(), NulError> { - extern { - pub fn send_message(jid: *const c_char, - password: *const c_char, - message: *const c_char, - to: *const c_char); - } - let cjid = try!(CString::new(jid)); - let cpassword = try!(CString::new(password)); - let cmessage = try!(CString::new(message)); - let cto = try!(CString::new(to)); - unsafe { - send_message(cjid.as_ptr(), cpassword.as_ptr(), cmessage.as_ptr(), cto.as_ptr()); - } - Ok(()) -}
\ No newline at end of file diff --git a/rust/src/token.rs b/rust/src/token.rs deleted file mode 100644 index 2a2e446..0000000 --- a/rust/src/token.rs +++ /dev/null @@ -1,73 +0,0 @@ -///! Token generation -use std::iter::*; - -use crypto::bcrypt::bcrypt; - -pub struct TokenGenerator { - /// Salt for bcrypt - salt: Vec<u8>, - /// bcrypt cost factor, defaults to 10 - bcrypt_cost: u32, - // length of a tokens valid time in seconds - pub valid_duration_secs: i64, -} - -impl TokenGenerator { - pub fn new(valid_duration_secs: i64, salt: Vec<u8>) -> TokenGenerator { - TokenGenerator { - salt: salt, - bcrypt_cost: 10, - valid_duration_secs: valid_duration_secs - } - } - - /// Return (from, to, token) - pub fn generate_token(&self, username: &str, at_time: i64) -> (i64, i64, String) { - let timeslot = at_time - (at_time % self.valid_duration_secs); - let input: String = format!("{}{}", username, timeslot); - return (timeslot, timeslot + self.valid_duration_secs, self.make_hash_token(&input.as_bytes())) - } - - #[inline(always)] - pub fn generate_token_norm(&self, username: &str, at_time: i64) -> (i64, i64, String) { - let (valid_from, valid_to, tok) = self.generate_token(username, at_time); - return (valid_from, valid_to, normalize_token(tok.as_str())); - } - - fn make_hash_token(&self, input: &[u8]) -> String { - let mut out = [0u8; 24]; - bcrypt(self.bcrypt_cost, &self.salt, input, &mut out); - let fold_func = { |acc, &e| acc ^ e }; - return format!("{:02X}-{:02X}-{:02X}", - out[0..7].into_iter().fold(0xff, &fold_func), - out[8..15].into_iter().fold(0xff, &fold_func), - out[16..23].into_iter().fold(0xff, &fold_func)) - } -} - - -pub fn normalize_token(token: &str) -> String { - token.to_lowercase().chars().filter(|c| c.is_digit(16)).collect::<String>() -} - -#[cfg(test)] -mod tests { - use super::*; - - - #[test] - fn test_normalize_token() { - assert_eq!(normalize_token(&"7A-74-F4".to_string()), "7a74f4"); - } - - #[test] - fn test_generate_token() { - use time; - let tg = TokenGenerator::new(time::Duration::hours(2).num_seconds(), - vec!(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)); - let (valid_from, valid_until, result) = tg.generate_token("a", 99999999); - assert_eq!( valid_from, 99993600); - assert_eq!( valid_until, 100000800); - assert_eq!( result, "7A-74-F4"); - } -}
\ No newline at end of file |