diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/apachelog.rs | 45 | ||||
-rw-r--r-- | src/handler.rs | 348 | ||||
-rw-r--r-- | src/main.rs | 98 | ||||
-rw-r--r-- | src/message.rs | 22 | ||||
-rw-r--r-- | src/sendxmpp.rs | 21 | ||||
-rw-r--r-- | src/token.rs | 73 |
6 files changed, 607 insertions, 0 deletions
diff --git a/src/apachelog.rs b/src/apachelog.rs new file mode 100644 index 0000000..fb5d1a6 --- /dev/null +++ b/src/apachelog.rs @@ -0,0 +1,45 @@ +///! 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/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..97f5d5c --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,348 @@ +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/src/main.rs b/src/main.rs new file mode 100644 index 0000000..30d4f8b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,98 @@ +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/src/message.rs b/src/message.rs new file mode 100644 index 0000000..0af1a13 --- /dev/null +++ b/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/src/sendxmpp.rs b/src/sendxmpp.rs new file mode 100644 index 0000000..2db471e --- /dev/null +++ b/src/sendxmpp.rs @@ -0,0 +1,21 @@ +///! 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/src/token.rs b/src/token.rs new file mode 100644 index 0000000..2a2e446 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,73 @@ +///! 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 |