From 53b5c00e625bf700a21be1a3e2070d3b23f1bef4 Mon Sep 17 00:00:00 2001 From: Yves Fischer Date: Sat, 3 Sep 2016 02:00:54 +0200 Subject: first test version --- rust/.gitignore | 4 + rust/Cargo.toml | 17 +++++ rust/README.md | 20 +++++ rust/build.rs | 6 ++ rust/clib/sendxmpp.c | 88 ++++++++++++++++++++++ rust/src/apachelog.rs | 45 ++++++++++++ rust/src/handler.rs | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++ rust/src/main.rs | 79 ++++++++++++++++++++ rust/src/sendxmpp.rs | 21 ++++++ rust/src/token.rs | 71 ++++++++++++++++++ 10 files changed, 550 insertions(+) create mode 100644 rust/.gitignore create mode 100644 rust/Cargo.toml create mode 100644 rust/README.md create mode 100644 rust/build.rs create mode 100644 rust/clib/sendxmpp.c create mode 100644 rust/src/apachelog.rs create mode 100644 rust/src/handler.rs create mode 100644 rust/src/main.rs create mode 100644 rust/src/sendxmpp.rs create mode 100644 rust/src/token.rs diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..3876e6f --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,4 @@ +target/ + +# don't care about it now +Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..50dfdaf --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "auth_xmppmessage" +version = "0.1.0" +authors = ["Yves Fischer "] +build = "build.rs" + +[dependencies] +conduit = "0.7.4" +civet = "0.8.3" +time = "0.1.35" +base64 = "0.2.1" +rust-crypto = "0.2.36" +rand = "0.3" +getopts = "0.2" + +[build-dependencies] +gcc = "0.3.35" \ No newline at end of file diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..8f2c1b9 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,20 @@ +# auth-xmppessage + +### Compile + +It's written in rust, compile it with `cargo build` + +### Run + +``` +Usage: ./target/debug/auth_xmppmessage [options] + +Options: + -j, --jid JID bot jid + -p, --password PASSWORD + bot password + -u, --user USER add valid user + -s, --secret SECRET server secret for token generation + -t, --time HOURS Validity of the token in hours (default 48) + -h, --help print this help menu +``` \ No newline at end of file diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..3e9c14f --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,6 @@ +extern crate gcc; + +fn main() { + gcc::compile_library("libsendxmpp.a", &["clib/sendxmpp.c"]); + println!("cargo:rustc-link-lib=strophe") +} \ No newline at end of file diff --git a/rust/clib/sendxmpp.c b/rust/clib/sendxmpp.c new file mode 100644 index 0000000..bfd4079 --- /dev/null +++ b/rust/clib/sendxmpp.c @@ -0,0 +1,88 @@ +#include + +struct message { + const char * to; + const char * message; +}; + +static void xmpp_send_message(xmpp_conn_t *conn, struct message * const msg) { + xmpp_stanza_t *x_msg, *x_body, *x_text; + xmpp_ctx_t *ctx = xmpp_conn_get_context(conn); + + x_msg = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(x_msg, "message"); + xmpp_stanza_set_type(x_msg, "chat"); + xmpp_stanza_set_attribute(x_msg, "to", msg->to); + + x_body = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(x_body, "body"); + + x_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(x_text, msg->message); + xmpp_stanza_add_child(x_body, x_text); + xmpp_stanza_add_child(x_msg, x_body); + + xmpp_send(conn, x_msg); + xmpp_stanza_release(x_msg); +} + +static void conn_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, + const int error, xmpp_stream_error_t * const stream_error, + void * const userdata) { + if (status == XMPP_CONN_CONNECT) { + fprintf(stderr, "DEBUG: connected\n"); + xmpp_send_message(conn, (struct message*) userdata); + xmpp_disconnect(conn); + } else { + xmpp_ctx_t * ctx = xmpp_conn_get_context(conn); + fprintf(stderr, "DEBUG: disconnected\n"); + xmpp_stop(ctx); + } +} + +void send_message(const char * jid, + const char * password, + const char * message, + const char * to) { + xmpp_conn_t *conn; + xmpp_log_t *log; + xmpp_ctx_t * ctx; + struct message * const msg = (struct message *)alloca(sizeof(struct message)); + msg->to = to; + msg->message = message; + + // TODO: Wait for version 0.8.9 +// long flags = XMPP_CONN_FLAG_MANDATORY_TLS; + + /* init library */ + xmpp_initialize(); + + /* create a context */ + log = xmpp_get_default_logger(XMPP_LEVEL_DEBUG); /* pass NULL instead to silence output */ + ctx = xmpp_ctx_new(NULL, log); + + /* create a connection */ + conn = xmpp_conn_new(ctx); + + /* configure connection properties (optional) */ + // TODO: Wait for version 0.8.9 +// xmpp_conn_set_flags(conn, flags); + + /* setup authentication information */ + xmpp_conn_set_jid(conn, jid); + xmpp_conn_set_pass(conn, password); + + /* initiate connection */ + xmpp_connect_client(conn, NULL, 0, conn_handler, (void*)msg); + + /* enter the event loop - + our connect handler will trigger an exit */ + xmpp_run(ctx); + + /* release our connection and context */ + xmpp_conn_release(conn); + xmpp_ctx_free(ctx); + + /* final shutdown of the library */ + xmpp_shutdown(); +} diff --git a/rust/src/apachelog.rs b/rust/src/apachelog.rs new file mode 100644 index 0000000..2517979 --- /dev/null +++ b/rust/src/apachelog.rs @@ -0,0 +1,45 @@ +///! Prints logging similar to apache http access.log +use std::net::IpAddr; +use conduit::{Request, Response}; +use time; + +pub struct LogEntry { + remote_ip_address: IpAddr, + remote_user: String, + request_path: String, + time: time::Tm, + status: u32, + 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.path()), + time: time::now(), + status: 0, + response_size: 0, + }; + return entry + } + + pub fn done(&mut self, response: Response) -> Response { + let (status_code, _) = response.status; + self.status = status_code; + self.print(); + return response; + } + + #[inline(always)] + fn print(&self) { + println!("{} - {} - [{}] \"{}\" {} {}", + 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 new file mode 100644 index 0000000..e16899b --- /dev/null +++ b/rust/src/handler.rs @@ -0,0 +1,199 @@ +use std::thread; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; +use std::io::empty; +use std::error::Error; +use std::cell::Cell; +use std::marker::Sync; +use std::ops::Add; +use std::time::Duration; + +use time; +use base64; +use civet::response; +use conduit::{Request, Response, Handler}; + +use token; +use sendxmpp; +use apachelog; + + +pub struct AuthHandler { + bot_jid: String, + bot_password: String, + usernames: HashSet, + valid_tokens_cache: Arc>>, + tg: token::TokenGenerator, + last_interactive_request: Cell, + headers_authenticate: HashMap>, +} + +impl AuthHandler { + pub fn make(bot_jid: String, bot_password: String, + usernames: HashSet, validity: time::Duration, secret: Vec) -> 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), + headers_authenticate: vec!(("WWW-Authenticate".to_string(), + vec!("Basic realm=\"xmppmessage auth\"".to_string()))) + .into_iter().collect(), + } + } + + 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"); + } + } + + // Result<(method, username, password), error-message> + fn _get_username_password(request: &Request) -> Result<(String, String, String), &'static str> { + let headers = request.headers(); + let mut auth_header = try!(headers.find("Authorization").ok_or("No Authorization header found")); + let authorization = try!(auth_header.pop().ok_or("No Authorization header value")); + 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!(base64::decode(value).or(Err("Fail base64 decode"))); + let utf8_decoded_value = try!(String::from_utf8(decoded_value).or(Err("Failed to utf-8 decode username/password"))); + 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((method_value.to_string(), username.to_string(), password.to_string())) + } + + fn _call_internal(&self, req: &Request) -> Result<(), (u32, &'static str)> { + let current_time = time::now().to_timespec().sec; + return match AuthHandler::_get_username_password(req) { + Ok((_, username, password)) => { + let is_known_user = self.usernames.contains(&username); + if username.len() > 0 && 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)); + return Err((429, "Too many requests")) + } else { + self.last_interactive_request.set(current_time); + if is_known_user { + self.send_message(&username); + } + return Err((401, "Token sent, retry now")) + } + } else { + match self.verify(&username, &password) { + Ok(true) => { + if is_known_user { + return Ok(()); + } else { + self.last_interactive_request.set(current_time); + Err((401, "Token sent, retry")) + } + }, + 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)); + return Err((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), + }; + return Err((401, "Authentication failed, username or password wrong")); + } + }, + Err(msg) => { + println!("verify failed: {}", msg); + Err((500, "Server Error")) + } + } + } + }, + Err(e) => { + println!("Failed: {}", e); + return Err((401, e)) + }, + }; + } + + fn clean_cache(&self) -> Result { + let now = time::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::>(); + let num = outdated_keys.iter().map(move |key| cache.remove(key)).count(); + Ok(num) + } + + fn verify(&self, username: &str, password: &str) -> Result { + let pw_token = token::normalize_token(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; + + // 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 = time::get_time().sec - self.tg.valid_duration_secs; + let (valid_until1, token1) = self.tg.generate_token_norm(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); + 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); + 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); + cache.insert(key, valid_until2); + return Ok(true) + } + } + println!("Invalid token for {}", username); + Ok(false) + } + } + } +} + +unsafe impl Sync for AuthHandler { +} + +impl Handler for AuthHandler { + fn call(&self, req: &mut Request) -> Result> { + let mut logentry = apachelog::LogEntry::start(req); + return match self._call_internal(req) { + Ok(_) => Ok(response((200, "OK, go ahead"), HashMap::new(), empty())), + Err((code, message)) => { + Ok(response((code, message), self.headers_authenticate.clone(), empty())) + } + }.map(|r| logentry.done(r)) + } +} \ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..9537bd7 --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,79 @@ +use std::env; +use std::collections::HashSet; +use std::iter::repeat; +use std::sync::mpsc::channel; + +extern crate base64; +extern crate crypto; +extern crate civet; +extern crate conduit; +extern crate getopts; +extern crate time; +extern crate rand; + +use civet::{Config, Server}; +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use getopts::Options; +use rand::{thread_rng, Rng}; + +mod apachelog; +mod handler; +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 = 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.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("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")) { + print_usage(&program, opts); + panic!("Missing jid or password"); + } + + let mut server_config = Config::new(); + let usernames = matches.opt_strs("u").into_iter().collect::>(); + let mut hasher = Sha1::new(); + let mut secret: Vec = 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 validity: i64 = matches.opt_str("t").unwrap_or(String::from("48")).parse() + .unwrap_or_else(|_| { panic!("Failed to parse time") }); + server_config.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, + time::Duration::hours(validity), + secret); + let _a = Server::start(server_config, handler); + let (_tx, rx) = channel::<()>(); + rx.recv().unwrap(); +} \ No newline at end of file diff --git a/rust/src/sendxmpp.rs b/rust/src/sendxmpp.rs new file mode 100644 index 0000000..2db471e --- /dev/null +++ b/rust/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/rust/src/token.rs b/rust/src/token.rs new file mode 100644 index 0000000..8b4ab5b --- /dev/null +++ b/rust/src/token.rs @@ -0,0 +1,71 @@ +///! Token generation +use std::iter::*; + +use crypto::bcrypt::bcrypt; + +pub struct TokenGenerator { + /// Salt for bcrypt + salt: Vec, + /// 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) -> TokenGenerator { + TokenGenerator { + salt: salt, + bcrypt_cost: 10, + valid_duration_secs: valid_duration_secs + } + } + + pub fn generate_token(&self, username: &str, at_time: 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())) + } + + 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())); + } + + 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::() +} + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_normalize_token() { + println!("{}", normalize_token(&"7A-74-F4".to_string())); + assert!(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_until, result) = tg.generate_token("a", 99999999); + assert!( valid_until == 100000800); + assert!( result == "7A-74-F4"); + } +} \ No newline at end of file -- cgit v1.2.1