diff options
author | Yves Fischer <yvesf-git@xapek.org> | 2016-12-15 20:50:23 +0100 |
---|---|---|
committer | Yves Fischer <yvesf-git@xapek.org> | 2016-12-17 15:10:46 +0100 |
commit | 6ec284ba70e736bc24523b279303cee46b06071a (patch) | |
tree | 5ab18399d663b722bdc312430f37c39a49d9a810 | |
parent | 285f0b800d9428c19530a17e25be069e38e2542d (diff) | |
download | auth-xmppmessage-6ec284ba70e736bc24523b279303cee46b06071a.tar.gz auth-xmppmessage-6ec284ba70e736bc24523b279303cee46b06071a.zip |
Replace switch -u with header X-Allowed-Jid
-rw-r--r-- | rust/src/apachelog.rs | 4 | ||||
-rw-r--r-- | rust/src/handler.rs | 156 | ||||
-rw-r--r-- | rust/src/main.rs | 22 | ||||
-rw-r--r-- | rust/src/message.rs | 22 | ||||
-rw-r--r-- | rust/src/token.rs | 22 |
5 files changed, 150 insertions, 76 deletions
diff --git a/rust/src/apachelog.rs b/rust/src/apachelog.rs index f502d99..4ab03ea 100644 --- a/rust/src/apachelog.rs +++ b/rust/src/apachelog.rs @@ -28,13 +28,13 @@ impl LogEntry { } pub fn done<R>(&mut self, _: &Response<R>) where R: Read { - self.status = 0; // not accessible + self.status = 0; // statuscode is not accessible :( self.print(); } #[inline(always)] fn print(&self) { - println!("{} - {} - [{}] \"{}\" {} {}", + info!("{} - {} - [{}] \"{}\" {} {}", self.remote_ip_address, self.remote_user, time::strftime("%d/%b/%Y %T %z", &self.time).unwrap(), diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 5fb7a03..f3e3f54 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -1,110 +1,142 @@ use std::cell::Cell; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap}; use std::io; use std::marker::Sync; use std::ops::Add; use std::str::from_utf8; +use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::thread; -use std::time::Duration; +use std::time::Duration as StdDuration; use sendxmpp; -use time; +use time::{get_time, Duration as TimeDuration}; use token; +use message::format_message; -use tiny_http::{Request, Response, StatusCode, Header}; +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, - usernames: HashSet<String>, valid_tokens_cache: Arc<RwLock<HashMap<String, i64>>>, tg: token::TokenGenerator, - last_interactive_request: Cell<i64> + last_interactive_request: Cell<i64>, + nosend: bool } 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(stringify!(Header not found: $name))); + .next().ok_or(concat!("No Header found named: '", $name, "'"))); } - impl AuthHandler { - pub fn make(bot_jid: String, bot_password: String, - usernames: HashSet<String>, validity: time::Duration, secret: Vec<u8>) -> 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, - usernames: usernames, 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 } } fn send_message(&self, user_jid: &str) { - let (valid_until, token) = self.tg.generate_token(user_jid, time::get_time().sec); - let message = format!("Token: {} for username: {} valid until {}", - token, user_jid, valid_until); - if sendxmpp::send_message(self.bot_jid.as_str(), self.bot_password.as_str(), - message.as_str(), user_jid).is_err() { - println!("Failed to send message"); + 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"); + } } } // Result<(method, username, password), error-message> #[inline(always)] - fn _get_username_password<'a>(request: &'a Request) -> Result<(String, String, String), &'static str> { - let headers = request.headers(); - let auth_header: &Header = { try!(get_header!(headers, "Authorization")) }; + fn _get_username_password<'a>(headers: &'a [Header]) -> Result<HeaderInfos, &'static str> { + let auth_header = try!(get_header!(headers, HTTP_HEADER_AUTHORIZATION!())); let authorization: &str = auth_header.value.as_str(); + debug!("{}: {}", HTTP_HEADER_AUTHORIZATION!(), authorization); let mut authorization_split = authorization.split(' '); let method_value = try!(authorization_split.next().ok_or("No method in header value")); - let value = try!(authorization_split.next().ok_or("No username/password value in header value")); - let decoded_value = try!(value.from_base64().or(Err("Fail base64 decode"))); - let utf8_decoded_value = try!(from_utf8(&decoded_value).or(Err("fail to decode utf-8"))); + let encoded_value = try!(authorization_split.next().ok_or("No username/password value in header value")); + let decoded_value = try!(encoded_value.from_base64().or(Err("Failed base64 decode"))); + let utf8_decoded_value = try!(from_utf8(&decoded_value).or(Err("Failed to decode UTF-8"))); let mut username_password_split = utf8_decoded_value.split(':'); let username = try!(username_password_split.next().ok_or("No username in header")); let password = try!(username_password_split.next().ok_or("No password in header")); - Ok((String::from(method_value), String::from(username), String::from(password))) + + let allowed_jids_value: &str = try!(get_header!(headers, HTTP_HEADER_X_ALLOWED_JID!())).value.as_str(); + debug!("{}: {}", HTTP_HEADER_X_ALLOWED_JID!(), allowed_jids_value); + let allowed_jids_list: Vec<String> = allowed_jids_value.split(',').map(String::from).collect(); + Ok(HeaderInfos { + auth_username: String::from(username), + auth_password: String::from(password), + auth_method: String::from(method_value), + allowed_jids: allowed_jids_list, + }) } fn authenticate_response(status_code: u16) -> io::Result<EmptyResponse> { Ok(Response::new( StatusCode(status_code), vec![ - Header::from_bytes(&b"WWW-Authenticate"[..], &b"Basic realm=\"xmppmessage auth\""[..]).unwrap() + Header { + field: HeaderField::from_bytes(HTTP_HEADER_WWW_AUTHENTICATE!()).unwrap(), + value: AsciiString::from_str(r#"Basic realm="xmppmessage auth""#).unwrap() + } ], - io::empty(), - Some(0), - None, + io::empty(), None, None )) } fn _call_internal(&self, request: &Request) -> io::Result<EmptyResponse> { - let current_time = time::now().to_timespec().sec; - return match AuthHandler::_get_username_password(request) { - Ok((_, username, password)) => { - let is_known_user = self.usernames.contains(&username); - if username.len() > 0 && password.len() == 0 { + let current_time = get_time().sec; + return match AuthHandler::_get_username_password(request.headers()) { + Ok(headerinfos) => { + let is_known_user = headerinfos.allowed_jids.contains(&headerinfos.auth_username); + if headerinfos.auth_method != "Basic" { + error!("Invalid authentication method. Responding with 405"); + return AuthHandler::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 - thread::sleep(Duration::from_secs(5)); + info!("Too many invalid token-requests, sleep 5 seconds"); + thread::sleep(StdDuration::from_secs(5)); return AuthHandler::authenticate_response(429) // Too many requests } else { self.last_interactive_request.set(current_time); if is_known_user { - self.send_message(&username); + self.send_message(&headerinfos.auth_username); } return AuthHandler::authenticate_response(401) //Token sent, retry now } } else { - match self.verify(&username, &password) { + match self.verify(&headerinfos) { Ok(true) => { if is_known_user { return Ok(Response::empty(200)) // Ok @@ -116,34 +148,34 @@ impl AuthHandler { 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(Duration::from_secs(5)); + thread::sleep(StdDuration::from_secs(5)); return Ok(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) => println!("Removed {} cache entries", num), - Err(e) => println!("{}", e), + Ok(num) => debug!("Removed {} cache entries", num), + Err(e) => error!("{}", e), }; return AuthHandler::authenticate_response(401) // Authentication failed, username or password wrong } }, Err(msg) => { - println!("verify failed: {}", msg); + error!("verify failed: {}", msg); return Err(io::Error::new(io::ErrorKind::Other, "Server Error")) // Server Error } } } }, Err(e) => { - info!("{}. Request Authentication", e); + info!("Error: {}. Responding with 401", e); return AuthHandler::authenticate_response(401) // No Authorization header }, }; } fn clean_cache(&self) -> Result<usize, &'static str> { - let now = time::get_time().sec; + 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()) @@ -152,11 +184,11 @@ impl AuthHandler { Ok(num) } - fn verify(&self, username: &str, password: &str) -> Result<bool, &'static str> { - let pw_token = token::normalize_token(password); + 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 = String::from(username).add(":").add(pw_token.as_str()); - let current_time = time::now().to_timespec().sec; + let key = headerinfos.auth_username.clone().add(":").add(pw_token.as_str()); + let current_time = get_time().sec; // try cache: let result1 = { @@ -174,24 +206,24 @@ impl AuthHandler { match result1 { Ok(true) => Ok(true), _ => { - let t1 = time::get_time().sec - self.tg.valid_duration_secs; - let (valid_until1, token1) = self.tg.generate_token_norm(username, t1); + 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"))); - println!("Cache for {} until {}", username, valid_until1); + debug!("Cache for {} from {} until {}", headerinfos.auth_username, valid_from1, valid_until1); cache.insert(key, valid_until1); return Ok(true) } else { - let t2 = time::get_time().sec; - let (valid_until2, token2) = self.tg.generate_token_norm(username, t2); + 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"))); - println!("Cache for {} until {}", username, valid_until2); + debug!("Cache for {} from {} until {}", headerinfos.auth_username, valid_from2, valid_until2); cache.insert(key, valid_until2); return Ok(true) } } - println!("Invalid token for {}", username); + warn!("Invalid token for {}", headerinfos.auth_username); Ok(false) } } @@ -207,4 +239,22 @@ impl AuthHandler { } unsafe impl Sync for AuthHandler { +} + +#[cfg(test)] +mod tests { + use super::*; + use time::{Duration as TimeDuration}; + + #[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); + } }
\ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs index 6338557..7a05c8b 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,9 +1,9 @@ use std::env; -use std::collections::HashSet; use std::iter::repeat; use std::sync::Arc; use std::thread; +extern crate ascii; extern crate crypto; extern crate env_logger; extern crate getopts; @@ -20,6 +20,7 @@ use rand::{thread_rng, Rng}; mod apachelog; mod handler; +mod message; mod sendxmpp; mod token; @@ -36,10 +37,10 @@ fn main() { let mut opts = Options::new(); opts.optopt("j", "jid", "bot jid", "JID"); opts.optopt("p", "password", "bot password", "PASSWORD"); - opts.optmulti("u", "user", "add valid user", "USER"); 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("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())); @@ -48,12 +49,11 @@ fn main() { return; } - if !(matches.opt_present("j") && matches.opt_present("p")) { + if !(matches.opt_present("j") && matches.opt_present("p")) && !matches.opt_present("n") { print_usage(&program, opts); panic!("Missing jid or password"); } - let usernames = matches.opt_strs("u").into_iter().collect::<HashSet<String>>(); 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| { @@ -70,20 +70,20 @@ fn main() { 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(), - matches.opt_str("p").unwrap(), - usernames, + 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); + 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 handles = Vec::new(); + let mut threads = Vec::new(); for _ in 0..2 { let server = server.clone(); let handler = handler.clone(); - handles.push(thread::spawn(move || { + threads.push(thread::spawn(move || { for request in server.incoming_requests() { let mut log = apachelog::LogEntry::start(&request); let response = handler.call(&request); @@ -92,7 +92,7 @@ fn main() { } })); } - for h in handles { + 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 new file mode 100644 index 0000000..0af1a13 --- /dev/null +++ b/rust/src/message.rs @@ -0,0 +1,22 @@ +///! 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/token.rs b/rust/src/token.rs index 8b4ab5b..2a2e446 100644 --- a/rust/src/token.rs +++ b/rust/src/token.rs @@ -21,15 +21,17 @@ impl TokenGenerator { } } - pub fn generate_token(&self, username: &str, at_time: i64) -> (i64, String) { + /// 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 + self.valid_duration_secs, self.make_hash_token(&input.as_bytes())) + return (timeslot, timeslot + self.valid_duration_secs, self.make_hash_token(&input.as_bytes())) } - pub fn generate_token_norm(&self, username: &str, at_time: i64) -> (i64, String) { - let (valid, tok) = self.generate_token(username, at_time); - return (valid, normalize_token(tok.as_str())); + #[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 { @@ -55,8 +57,7 @@ mod tests { #[test] fn test_normalize_token() { - println!("{}", normalize_token(&"7A-74-F4".to_string())); - assert!(normalize_token(&"7A-74-F4".to_string()) == "7a74f4"); + assert_eq!(normalize_token(&"7A-74-F4".to_string()), "7a74f4"); } #[test] @@ -64,8 +65,9 @@ mod tests { 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_until, result) = tg.generate_token("a", 99999999); - assert!( valid_until == 100000800); - assert!( result == "7A-74-F4"); + 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 |