summaryrefslogtreecommitdiff
path: root/rust/src
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src')
-rw-r--r--rust/src/apachelog.rs45
-rw-r--r--rust/src/handler.rs348
-rw-r--r--rust/src/main.rs98
-rw-r--r--rust/src/message.rs22
-rw-r--r--rust/src/sendxmpp.rs21
-rw-r--r--rust/src/token.rs73
6 files changed, 0 insertions, 607 deletions
diff --git a/rust/src/apachelog.rs b/rust/src/apachelog.rs
deleted file mode 100644
index fb5d1a6..0000000
--- a/rust/src/apachelog.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-///! 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/rust/src/handler.rs b/rust/src/handler.rs
deleted file mode 100644
index 97f5d5c..0000000
--- a/rust/src/handler.rs
+++ /dev/null
@@ -1,348 +0,0 @@
-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 > &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 = 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/rust/src/main.rs b/rust/src/main.rs
deleted file mode 100644
index 30d4f8b..0000000
--- a/rust/src/main.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-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/rust/src/message.rs b/rust/src/message.rs
deleted file mode 100644
index 0af1a13..0000000
--- a/rust/src/message.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-///! 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/sendxmpp.rs b/rust/src/sendxmpp.rs
deleted file mode 100644
index 2db471e..0000000
--- a/rust/src/sendxmpp.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-///! 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
deleted file mode 100644
index 2a2e446..0000000
--- a/rust/src/token.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-///! 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