summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYves Fischer <yvesf-git@xapek.org>2016-12-15 20:50:23 +0100
committerYves Fischer <yvesf-git@xapek.org>2016-12-17 15:10:46 +0100
commit6ec284ba70e736bc24523b279303cee46b06071a (patch)
tree5ab18399d663b722bdc312430f37c39a49d9a810
parent285f0b800d9428c19530a17e25be069e38e2542d (diff)
downloadauth-xmppmessage-6ec284ba70e736bc24523b279303cee46b06071a.tar.gz
auth-xmppmessage-6ec284ba70e736bc24523b279303cee46b06071a.zip
Replace switch -u with header X-Allowed-Jid
-rw-r--r--rust/src/apachelog.rs4
-rw-r--r--rust/src/handler.rs156
-rw-r--r--rust/src/main.rs22
-rw-r--r--rust/src/message.rs22
-rw-r--r--rust/src/token.rs22
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