summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorYves Fischer <yvesf-git@xapek.org>2016-09-03 02:00:54 +0200
committerYves Fischer <yvesf-git@xapek.org>2016-09-03 12:26:30 +0200
commit53b5c00e625bf700a21be1a3e2070d3b23f1bef4 (patch)
treefaace67aba86121a28382a0c74fe7701e4d398b7 /rust
parent311a02c31e03b23cc66e662cce63e6c19d4bc662 (diff)
downloadauth-xmppmessage-53b5c00e625bf700a21be1a3e2070d3b23f1bef4.tar.gz
auth-xmppmessage-53b5c00e625bf700a21be1a3e2070d3b23f1bef4.zip
first test version
Diffstat (limited to 'rust')
-rw-r--r--rust/.gitignore4
-rw-r--r--rust/Cargo.toml17
-rw-r--r--rust/README.md20
-rw-r--r--rust/build.rs6
-rw-r--r--rust/clib/sendxmpp.c88
-rw-r--r--rust/src/apachelog.rs45
-rw-r--r--rust/src/handler.rs199
-rw-r--r--rust/src/main.rs79
-rw-r--r--rust/src/sendxmpp.rs21
-rw-r--r--rust/src/token.rs71
10 files changed, 550 insertions, 0 deletions
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 <yvesf+git@xapek.org>"]
+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 <strophe.h>
+
+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<String>,
+ valid_tokens_cache: Arc<RwLock<HashMap<String, i64>>>,
+ tg: token::TokenGenerator,
+ last_interactive_request: Cell<i64>,
+ headers_authenticate: HashMap<String, Vec<String>>,
+}
+
+impl AuthHandler {
+ pub fn make(bot_jid: String, bot_password: String,
+ usernames: HashSet<String>, validity: time::Duration, secret: Vec<u8>) -> 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<usize, &'static str> {
+ 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::<Vec<_>>();
+ let num = outdated_keys.iter().map(move |key| cache.remove(key)).count();
+ Ok(num)
+ }
+
+ fn verify(&self, username: &str, password: &str) -> Result<bool, &'static str> {
+ 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 > &current_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<Response, Box<Error + Send>> {
+ 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<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.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::<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| {
+ 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<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
+ }
+ }
+
+ 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::<String>()
+}
+
+#[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