From 3b89dc69da0f88cf8e2290523fa50656ac2ebb5d Mon Sep 17 00:00:00 2001 From: Yves Fischer Date: Mon, 26 Nov 2018 01:35:11 +0100 Subject: Proof of concept with totp --- src/auth_handler/handler_info.rs | 30 ++++ src/auth_handler/handler_login.rs | 142 ++++++++++++++++++ src/auth_handler/mod.rs | 170 ++++++++++++++++++++++ src/auth_handler/urls.rs | 18 +++ src/cookie_store.rs | 133 +++++++++++++++++ src/http_server.rs | 298 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 106 ++++++++++++++ src/router.rs | 162 +++++++++++++++++++++ src/system.rs | 8 + src/totp.rs | 30 ++++ 10 files changed, 1097 insertions(+) create mode 100644 src/auth_handler/handler_info.rs create mode 100644 src/auth_handler/handler_login.rs create mode 100644 src/auth_handler/mod.rs create mode 100644 src/auth_handler/urls.rs create mode 100644 src/cookie_store.rs create mode 100644 src/http_server.rs create mode 100644 src/main.rs create mode 100644 src/router.rs create mode 100644 src/system.rs create mode 100644 src/totp.rs (limited to 'src') diff --git a/src/auth_handler/handler_info.rs b/src/auth_handler/handler_info.rs new file mode 100644 index 0000000..0aeaa09 --- /dev/null +++ b/src/auth_handler/handler_info.rs @@ -0,0 +1,30 @@ +use std::io; + +use tokio::prelude::*; + +use http::{Request, Response, StatusCode}; +use bytes::Bytes; + +use ::ApplicationState; +use super::AuthHandler; + +pub(in super) fn respond<'a>(auth_handler: &AuthHandler, state: &ApplicationState, req: &Request, + path_rest: &'a str) -> Response { + let body = html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "Hello world!"; + } + body { + h1(id = "heading") { + : "Hello! This is "; + : "And path rest is: "; + : path_rest; + : "... ok :)"; + } + } + } + }; + Response::builder().body(body.to_string()).unwrap() +} \ No newline at end of file diff --git a/src/auth_handler/handler_login.rs b/src/auth_handler/handler_login.rs new file mode 100644 index 0000000..83d5214 --- /dev/null +++ b/src/auth_handler/handler_login.rs @@ -0,0 +1,142 @@ +use std::io; +use std::borrow::Cow; + +use tokio::prelude::*; + +use http::{Request, Response, StatusCode, Method}; +use http::header::{SET_COOKIE, COOKIE}; +use url::form_urlencoded; + +use ::ApplicationState; +use ::totp; +use super::*; + + +pub(in super) fn GET<'a>(header_infos: &HeaderExtract, state: &ApplicationState, path_rest: &'a str) + -> Response { + let body = if is_logged_in(&header_infos.cookies, &state.cookie_store) { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login"; + } + body { + h1(id = "heading") { + : "Currently logged in" + } + } + } + }) + } else { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login"; + } + body { + h1(id = "heading") { + : "Login" + } + form(method="POST") { + label(for="token") { + : "Enter TOTP token" + } + input(name="token",id="token",type="text"); + input(name="redirect", type="hidden", value=path_rest); + input(name="send",type="submit",value="Submit"); + } + } + } + }) + }; + make_response(StatusCode::OK, format!("{}", body)) +} + +fn test_secrets(secrets: &Vec<&str>, token: &String) -> bool { + secrets.iter() + .any(|secret| { + match totp::verify(secret, token) { + Ok(true) => true, + Ok(false) => false, + Err(e) => { + error!("Error from totp::verify: {}", e); + false + } + } + }) +} + +pub(in super) fn POST<'a>(header_infos: &HeaderExtract, state: &ApplicationState, req: &Request) + -> Response { + let mut token = None; + let mut redirect = None; + for (key, val) in form_urlencoded::parse(req.body()) { + if key == "token" { + token = Some(val.into_owned()) + } else if key == "redirect" { + redirect = Some(val.into_owned()) + } + } + if token.is_none() { + return error_handler_internal("missing argument 'token'".to_string()); + } + let redirect = redirect.unwrap_or(Default::default()); + + if header_infos.totp_secrets.is_empty() { + return error_handler_internal("no secrets configured".to_string()) + } + + let mut ret = Response::builder(); + let body = if test_secrets(&header_infos.totp_secrets, &token.unwrap()) { + let cookie_value = state.cookie_store.create_authenticated_cookie(); + let cookie = CookieBuilder::new(COOKIE_NAME, cookie_value.to_string()) + .http_only(true) + .path("/") + .max_age(state.cookie_max_age) + .finish(); + ret.header(SET_COOKIE, cookie.to_string()); + warn!("Authenticated user with cookie {}", cookie); + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Successful"; + meta(http-equiv="refresh", content=format!("3; URL={}", redirect)) + } + body { + h1(id = "heading") { + : "Login succesful" + } + a(href="login") { + : "Try again... redirecting to "; + } + span { + : format!("{}", redirect) + } + } + } + }) + } else { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login failed"; + meta(http-equiv="refresh", content="1") + } + body { + h1(id = "heading") { + : "Login failed" + } + a(href="login") { + : "Try again... " + } + } + } + }) + }; + + ret.body(body).unwrap() +} \ No newline at end of file diff --git a/src/auth_handler/mod.rs b/src/auth_handler/mod.rs new file mode 100644 index 0000000..b4e02d1 --- /dev/null +++ b/src/auth_handler/mod.rs @@ -0,0 +1,170 @@ +#![allow(warnings)] + +use std::cell::Cell; +use std::collections::HashMap; +use std::io; +use std::marker::Sync; +use std::str; +use std::str::FromStr; +use std::sync::{Arc, RwLock, Mutex, MutexGuard}; +use std::cell::RefCell; + +use time::{Tm, Duration}; +use http::{Request, Response, StatusCode, Method}; +use tokio::prelude::*; +use horrorshow; +use cookie::{Cookie,CookieBuilder}; +use bytes::Bytes; + +use router; +use cookie_store::CookieStore; +use cookie_store::to_cookie; +use http_server::HttpHandler; + +mod urls; +mod handler_login; +mod handler_info; + +pub(in auth_handler) struct HeaderExtract<'a> { + totp_secrets: Vec<&'a str>, + cookies: Vec>, +} + +pub struct HeaderMissing { + name: &'static str, +} + +static HTTP_HEADER_AUTHORIZATION: &'static str = r"Authorization"; +static HTTP_HEADER_X_ORIGINAL_URL: &'static str = r"X-Original-Url"; +static HTTP_HEADER_WWW_AUTHENTICATE: &'static str = r"WWW-Authenticate"; +static HTTP_HEADER_X_TOTP_SECRET: &'static str = r"X-Totp-Secret"; +static COOKIE_NAME: &'static str = r"totp_cookie"; + +#[derive(Clone)] +pub struct AuthHandler { + routing_table: router::RoutingTable, +} + +pub(in auth_handler) fn make_response(code: StatusCode, body: String) -> Response { + Response::builder().status(code).body(body).unwrap() +} + +pub(in auth_handler) fn error_handler_internal(body: String) -> Response { + Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(body).unwrap() +} + +impl HttpHandler for AuthHandler { + fn respond(&self, state: &super::ApplicationState, req: Request) -> Response { + match self.routing_table.match_path(req.uri().path()) { + Ok((urls::Route::Info, rest)) => handler_info::respond(self, state, &req, rest), + Ok((urls::Route::Login, rest)) => self.login(state, &req, rest), + Ok((urls::Route::Logout, rest)) => self.logout(state, &req, rest), + Ok((urls::Route::Check, rest)) => self.check(state, &req, rest), + Err(error) => match error { + router::NoMatchingRoute => + make_response(StatusCode::NOT_FOUND, "Resource not found".to_string()), + } + } + } +} + +pub fn is_logged_in(cookies: &Vec, cookie_store: &CookieStore) -> bool { + for cookie in cookies { + if cookie.name() == COOKIE_NAME { + let cookie_value = to_cookie(cookie.value()); + if cookie_value.is_some() && cookie_store.is_cookie_authenticated(&cookie_value.unwrap()) { + return true; + } + } + } + false +} + + +impl AuthHandler { + pub fn make() -> AuthHandler { + AuthHandler { routing_table: urls::create_routing_table() } + } + + fn login<'a>(&self, state: &super::ApplicationState, req: &Request, path_rest: &'a str, + ) -> Response { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + match *req.method() { + Method::GET => handler_login::GET(&header_infos, state, path_rest), + Method::POST => handler_login::POST(&header_infos, state, req), + _ => error_handler_internal("Wrong method".to_string()), + } + } + + fn logout<'a>(&self, state: &super::ApplicationState, req: &Request, path_rest: &'a str, + ) -> Response { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + + let body = format!("Rest: {}", path_rest); + Response::builder().body(body.to_string()).unwrap() + } + + + fn check<'a>(&self, state: &super::ApplicationState, req: &Request, path_rest: &'a str) -> Response { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + if is_logged_in(&header_infos.cookies, &state.cookie_store) { + make_response(StatusCode::OK, "".to_string()) + } else { + make_response(StatusCode::UNAUTHORIZED, "Cookie expired".to_string()) + } + } + + + fn parse_header_infos(req: &Request) -> Result { + let mut totp_secrets = Vec::new(); + for header_value in req.headers().get_all(HTTP_HEADER_X_TOTP_SECRET) { + let value = header_value.to_str().or(Err("Failed to read totp-secret header value"))?; + totp_secrets.push(value); + } + + let mut cookies = Vec::new(); + for header_value in req.headers().get_all(::http::header::COOKIE) { + let value = header_value.to_str().or(Err("Failed to read cookie value"))?; + let cookie = Cookie::parse(value).or(Err("Failed to parse cookie value"))?; + cookies.push(cookie); + } + + Ok(HeaderExtract { totp_secrets, cookies }) + } +} + +#[cfg(test)] +mod test1 { + // use super::*; + use test::Bencher; + // use horrorshow::prelude::*; + use horrorshow::helper::doctype; + + #[bench] + fn bench_1(_: &mut Bencher) { + let _ = format!("{}", html! { +: doctype::HTML; +html { +head { +title: "Hello world!"; +} +body { +// attributes +h1(id = "heading") { +// Insert escaped text +: "Hello! This is " +} +} +} +}); + } +} diff --git a/src/auth_handler/urls.rs b/src/auth_handler/urls.rs new file mode 100644 index 0000000..fa7c76f --- /dev/null +++ b/src/auth_handler/urls.rs @@ -0,0 +1,18 @@ +use ::router; + +#[derive(Clone, Copy)] +pub(in auth_handler) enum Route { + Login, + Logout, + Info, + Check +} + +pub(in auth_handler) fn create_routing_table() -> router::RoutingTable { + let mut r = router::RoutingTable::new(); + r.insert("/info", Route::Info); + r.insert("/login", Route::Login); + r.insert("/logout", Route::Logout); + r.insert("/check", Route::Check); + r +} \ No newline at end of file diff --git a/src/cookie_store.rs b/src/cookie_store.rs new file mode 100644 index 0000000..a77bb14 --- /dev/null +++ b/src/cookie_store.rs @@ -0,0 +1,133 @@ +use evmap; +use std::sync::{Arc, Mutex, MutexGuard}; +use evmap::{WriteHandle, ReadHandle}; +use std::time; +use std::str; +use std::hash; + +use random; +use random::Source; + +// cookie is a 64-byte printable-characters-only array +pub struct CookieKey([u8; 64]); + +impl PartialEq for CookieKey { + fn eq(&self, other: &CookieKey) -> bool { + self.0[..] == other.0[..] + } +} + +impl Eq for CookieKey {} + +impl hash::Hash for CookieKey { + fn hash(&self, state: &mut H) { + for i in self.0.iter() { + state.write_u8(*i) + } + } +} + +impl Clone for CookieKey { + fn clone(&self) -> CookieKey { + CookieKey(self.0) + } +} + +impl ToString for CookieKey { + fn to_string(&self) -> String { + unsafe { str::from_utf8_unchecked(&self.0).to_string() } + } +} + +pub struct CookieStore { + pub reader: ReadHandle, + pub writer: Arc>>, +} + +pub fn to_cookie(data: &str) -> Option { + if data.len() == 64 { + let mut cookie_key: [u8; 64] = [0; 64]; + cookie_key.copy_from_slice(data.as_bytes()); + Some(CookieKey(cookie_key)) + } else { + None + } +} + +static HEXTABLE: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', + b'A', b'B', b'C', b'D', b'E', b'F']; + + +impl Clone for CookieStore { + fn clone(&self) -> Self { + CookieStore { + reader: self.reader.clone(), + writer: self.writer.clone(), + } + } +} + +impl CookieStore { + pub fn new() -> CookieStore { + let (r, w) = evmap::new::(); + CookieStore { + reader: r, + writer: Arc::new(Mutex::new(w)), + } + } + + pub fn create_authenticated_cookie(&self) -> CookieKey { + let mut r = random::default(); + let mut key = [0; 64]; + for it in key.iter_mut() { + let random: u8 = r.read(); + let value = HEXTABLE[(random & 0x0f) as usize]; + *it = value; + } + + let timeout = time::SystemTime::now() + time::Duration::from_secs(60 * 60 * 24); // 1 day + let timeout = timeout.duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_secs(); + { + let mut writer = self.write_handle(); + writer.insert(CookieKey(key), timeout); + warn!("Insert: {}", CookieKey(key).to_string()); + writer.refresh(); + } + CookieKey(key) + } + + + fn write_handle(&self) -> MutexGuard> { + self.writer.lock().unwrap() + } + + fn now_unix_epoch() -> u64 { + time::SystemTime::now() + .duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_secs() + } + + /// true -> cookie is valid until time + /// false -> cookie is outdated + pub fn is_cookie_authenticated(&self, key: &CookieKey) -> bool { + let reader = &self.reader; + let value = reader.get_and(key, |v| v[0]); + + warn!("Reading {} -> {:?}", key.to_string(), value); + if value.is_none() { + false + } else if value.unwrap() < Self::now_unix_epoch() { + // outdated, remove from map + let mut writer = self.write_handle(); + writer.empty(key.clone()); + // but no refresh - it's not urgent + false + } else { + true + } + } + + pub fn clean_outdated_cookies(&self) { +// unimplemented!() + } +} + diff --git a/src/http_server.rs b/src/http_server.rs new file mode 100644 index 0000000..826163c --- /dev/null +++ b/src/http_server.rs @@ -0,0 +1,298 @@ +use std::fmt; +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; +use std::boxed::Box; +use bytes::Bytes; + +use tokio; +use tokio::net::TcpListener; +use tokio::prelude::*; +use tokio::codec::{Encoder, Decoder}; +use tokio_threadpool::Builder; +use tokio_executor::enter; +use bytes::BytesMut; +use http::header::HeaderValue; +use http::{Request, Response}; +use thread_local::ThreadLocal; + +use system; + +pub trait HttpHandler { + fn respond(&self, state: &T, req: Request) -> Response; +} + +pub fn serve< + T: 'static + Send + Clone + HttpHandler, + X: Send + Clone + 'static +>(addr: SocketAddr, state: X, handler: T) { + let listener = TcpListener::bind(&addr).expect("failed to bind"); + info!("Listening on: {}", addr); + let tl_handler: Arc> = Arc::new(ThreadLocal::new()); + let tl_state: Arc>= Arc::new(ThreadLocal::new()); + + let program = + listener.incoming() + .map_err(|e| error!("failed to accept socket; error = {:?}", e)) + .for_each(move |socket| { + let peer_addr = match socket.peer_addr() { + Ok(addr) => format!("{}", addr), + Err(_) => "".to_string(), + }; + + let (tx, rx) = + HttpFrame.framed(socket).split(); + + let tl_handler = tl_handler.clone(); + let handler = handler.clone(); + + let tl_state = tl_state.clone(); + let state = state.clone(); + + let rx_task = rx.and_then(move |req| { + let state = state.clone(); + let state = tl_state.get_or(||{ + debug!("Clone state"); + Box::new(state.clone()) + }); + let handler = tl_handler.get_or(|| { + debug!("Clone handler"); + Box::new(handler.clone()) + }); + info!("{:?} {} {} {:?}", peer_addr, req.method(), req.uri(), req.version()); + let response = handler.respond(&state, req); + Box::new(future::ok(response)) + }); + let tx_task = tx.send_all(rx_task) + .then(|res| { + if let Err(e) = res { + error!("failed to process connection; error = {:?}", e); + } + Ok(()) + }); + + // Spawn the task that handles the connection. + tokio::spawn(tx_task); + Ok(()) + }); + + + let mut builder = Builder::new(); + let runtime = builder + .name_prefix("httpd-") + .after_start(|| { + debug!("Start new worker"); + system::initialize_rng_from_time(); + }) + .build(); + runtime.spawn(program); + enter().expect("nested tokio::run") + .block_on(runtime.shutdown_on_idle()) + .unwrap(); +} + +/// +/// The following code is mostly copied from: +/// https://github.com/tokio-rs/tokio/blob/master/examples/tinyhttp.rs +///------------------------------------------------------------------------------------------------- +struct HttpFrame; + +/// Implementation of encoding an HTTP response into a `BytesMut`, basically +/// just writing out an HTTP/1.1 response. +impl Encoder for HttpFrame { + type Item = Response; + type Error = io::Error; + + fn encode(&mut self, item: Response, dst: &mut BytesMut) -> io::Result<()> { + use std::fmt::Write; + + write!(BytesWrite(dst), "\ + HTTP/1.1 {}\r\n\ + Server: nginx-auth-totp\r\n\ + Content-Length: {}\r\n\ + Date: {}\r\n\ + ", item.status(), item.body().len(), date::now()).unwrap(); + + for (k, v) in item.headers() { + dst.extend_from_slice(k.as_str().as_bytes()); + dst.extend_from_slice(b": "); + dst.extend_from_slice(v.as_bytes()); + dst.extend_from_slice(b"\r\n"); + } + + dst.extend_from_slice(b"\r\n"); + dst.extend_from_slice(item.body().as_bytes()); + + return Ok(()); + + // Right now `write!` on `Vec` goes through io::Write and is not + // super speedy, so inline a less-crufty implementation here which + // doesn't go through io::Error. + struct BytesWrite<'a>(&'a mut BytesMut); + + impl<'a> fmt::Write for BytesWrite<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.extend_from_slice(s.as_bytes()); + Ok(()) + } + + fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { + fmt::write(self, args) + } + } + } +} + +/// Implementation of decoding an HTTP request from the bytes we've read so far. +/// This leverages the `httparse` crate to do the actual parsing and then we use +/// that information to construct an instance of a `http::Request` object, +/// trying to avoid allocations where possible. +impl Decoder for HttpFrame { + type Item = Request; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> io::Result>> { + // TODO: we should grow this headers array if parsing fails and asks + // for more headers + let mut headers = [None; 16]; + let (method, path, version, amt) = { + let mut parsed_headers = [httparse::EMPTY_HEADER; 16]; + let mut r = httparse::Request::new(&mut parsed_headers); + let status = r.parse(src).map_err(|e| { + let msg = format!("failed to parse http request: {:?}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; + + let amt = match status { + httparse::Status::Complete(amt) => amt, + httparse::Status::Partial => return Ok(None), + }; + + let toslice = |a: &[u8]| { + let start = a.as_ptr() as usize - src.as_ptr() as usize; + assert!(start < src.len()); + (start, start + a.len()) + }; + + for (i, header) in r.headers.iter().enumerate() { + let k = toslice(header.name.as_bytes()); + let v = toslice(header.value); + headers[i] = Some((k, v)); + } + + (toslice(r.method.unwrap().as_bytes()), + toslice(r.path.unwrap().as_bytes()), + r.version.unwrap(), + amt) + }; + if version != 1 && version != 0 { // TODO + error!("Version: {}", version); + return Err(io::Error::new(io::ErrorKind::Other, "only HTTP/1.1 accepted")); + } + let data = src.split_to(amt).freeze(); + let mut req_builder = Request::builder(); + req_builder.method(&data[method.0..method.1]); + req_builder.uri(data.slice(path.0, path.1)); + req_builder.version(http::Version::HTTP_11); + for header in headers.iter() { + let (k, v) = match *header { + Some((ref k, ref v)) => (k, v), + None => break, + }; + let value = unsafe { + HeaderValue::from_shared_unchecked(data.slice(v.0, v.1)) + }; + req_builder.header(&data[k.0..k.1], value); + } + + + let request_body = src.split_off(0).freeze(); + let req = req_builder.body(request_body).map_err(|e| { + io::Error::new(io::ErrorKind::Other, e) + })?; + Ok(Some(req)) + } +} + +mod date { + use std::cell::RefCell; + use std::fmt::{self, Write}; + use std::str; + + use time::{self, Duration}; + + pub struct Now(()); + + /// Returns a struct, which when formatted, renders an appropriate `Date` + /// header value. + pub fn now() -> Now { + Now(()) + } + + // Gee Alex, doesn't this seem like premature optimization. Well you see + // there Billy, you're absolutely correct! If your server is *bottlenecked* + // on rendering the `Date` header, well then boy do I have news for you, you + // don't need this optimization. + // + // In all seriousness, though, a simple "hello world" benchmark which just + // sends back literally "hello world" with standard headers actually is + // bottlenecked on rendering a date into a byte buffer. Since it was at the + // top of a profile, and this was done for some competitive benchmarks, this + // module was written. + // + // Just to be clear, though, I was not intending on doing this because it + // really does seem kinda absurd, but it was done by someone else [1], so I + // blame them! :) + // + // [1]: https://github.com/rapidoid/rapidoid/blob/f1c55c0555007e986b5d069fe1086e6d09933f7b/rapidoid-commons/src/main/java/org/rapidoid/commons/Dates.java#L48-L66 + + struct LastRenderedNow { + bytes: [u8; 128], + amt: usize, + next_update: time::Timespec, + } + + thread_local!(static LAST: RefCell = RefCell::new(LastRenderedNow { + bytes: [0; 128], + amt: 0, + next_update: time::Timespec::new(0, 0), + })); + + impl fmt::Display for Now { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + LAST.with(|cache| { + let mut cache = cache.borrow_mut(); + let now = time::get_time(); + if now >= cache.next_update { + cache.update(now); + } + f.write_str(cache.buffer()) + }) + } + } + + impl LastRenderedNow { + fn buffer(&self) -> &str { + str::from_utf8(&self.bytes[..self.amt]).unwrap() + } + + fn update(&mut self, now: time::Timespec) { + self.amt = 0; + write!(LocalBuffer(self), "{}", time::at(now).rfc822()).unwrap(); + self.next_update = now + Duration::seconds(1); + self.next_update.nsec = 0; + } + } + + struct LocalBuffer<'a>(&'a mut LastRenderedNow); + + impl<'a> fmt::Write for LocalBuffer<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let start = self.0.amt; + let end = start + s.len(); + self.0.bytes[start..end].copy_from_slice(s.as_bytes()); + self.0.amt += s.len(); + Ok(()) + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bb3c57e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,106 @@ +#![feature(test)] +#![feature(convert_id)] +#![feature(proc_macro_hygiene)] +#![feature(try_from)] +#![feature(duration_as_u128)] +#![feature(libc)] + +use std::env; +use std::sync::Arc; +use std::thread; +use std::sync::atomic; +use std::net::SocketAddr; + +extern crate ascii; +extern crate getopts; +#[macro_use] +extern crate log; +extern crate tokio; +extern crate tokio_threadpool; +extern crate tokio_executor; +extern crate time; +extern crate simple_logger; +extern crate oath; +extern crate evmap; +extern crate test; +#[macro_use] +extern crate horrorshow; +extern crate random; +extern crate http; +extern crate httparse; +extern crate bytes; +extern crate thread_local; +extern crate cookie; +extern crate url; + +use getopts::Options; +use log::LogLevel::{Debug, Warn}; +use time::Duration; + +mod auth_handler; +mod cookie_store; +mod http_server; +mod router; +mod system; +mod totp; + +extern crate libc; + +use cookie_store::CookieStore; + +#[derive(Clone)] +pub struct ApplicationState { + cookie_store: CookieStore, + cookie_max_age: Duration, +} + +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("l", "port", "Listen address", "LISTEN-ADDR"); + opts.optflag("d", "debug", "Use loglevel Debug instead of Warn"); + 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; + } + + simple_logger::init_with_level(if matches.opt_present("d") { Debug } else { Warn }) + .unwrap_or_else(|_| panic!("Failed to initialize logger")); + + + let addr = matches.opt_str("l").unwrap_or_else(||"127.0.0.1:8080".to_string()); + let addr = addr.parse::() + .unwrap_or_else(|_| panic!("Failed to parse LISTEN-ADDRESS")); + + + // concurrent eventual consistent hashmap with + let state = ApplicationState { cookie_store: CookieStore::new(), cookie_max_age: Duration::days(1) }; + + let server_shutdown_condvar = Arc::new(atomic::AtomicBool::new(false)); + + let cookie_clean_thread_condvar = server_shutdown_condvar.clone(); + let cookie_clean_state = state.clone(); + let cookie_clean_thread = thread::spawn(move || { + while !cookie_clean_thread_condvar.load(atomic::Ordering::Relaxed) { + thread::sleep(std::time::Duration::from_secs(60)); + debug!("Clean cookie cache"); + cookie_clean_state.cookie_store.clean_outdated_cookies(); + } + }); + + let auth_handler = auth_handler::AuthHandler::make(); + http_server::serve(addr, state, auth_handler); + + server_shutdown_condvar.store(true, atomic::Ordering::Relaxed); + debug!("Waiting for cleanup thread to shutdown"); + cookie_clean_thread.join().unwrap(); +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..18ed429 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,162 @@ +use std::collections::VecDeque; + +#[derive(Clone, Copy)] +enum TablePointer where R: Clone + Copy { + Link(usize), + Route(R), + RouteWithLink(usize, R), + NotFound, +} + +pub struct NoMatchingRoute; + +pub type RouteMatch<'a, R> = Result<(R, &'a str), NoMatchingRoute>; + +#[derive(Clone)] +pub struct RoutingTable where R: Copy { + tables: Vec<[TablePointer; 128]>, +} + +impl RoutingTable where R: Copy { + pub fn new() -> RoutingTable { + let zero_table = [TablePointer::NotFound; 128]; + RoutingTable { + tables: vec![zero_table], + } + } + + pub fn insert(&mut self, path: &str, route: R) { + assert!(!path.is_empty()); + let mut index: usize = 0; + let p: Vec = path.as_bytes().iter().map(|i| usize::from(*i)).collect(); + let mut path_queue = VecDeque::from(p); + + while path_queue.len() > 1 { + let ch = path_queue.pop_front().unwrap() as usize % 128; + match self.tables[index][ch] { + TablePointer::NotFound => { + self.tables.push([TablePointer::NotFound; 128]); + let i_other_table = self.tables.len() - 1; + self.tables[index][ch] = TablePointer::Link(i_other_table); + path_queue.push_front(ch); + } + TablePointer::Link(i_other_table) => { + index = i_other_table as usize; + } + TablePointer::RouteWithLink(i_other_table, _) => { + index = i_other_table as usize; + path_queue.push_front(ch); + } + TablePointer::Route(route) => { + self.tables.push([TablePointer::NotFound; 128]); + let i_other_table = self.tables.len() - 1; + self.tables[index][ch] = TablePointer::RouteWithLink(i_other_table, route); + path_queue.push_front(ch); + } + } + } + // last character element in path + let ch = path_queue.pop_front().unwrap() % 128; + match self.tables[index][ch] { + TablePointer::NotFound => { + // slot is empty, just place the Route + self.tables[index][ch] = TablePointer::Route(route) + } + TablePointer::Link(i) => { + // slot is filled with link to longer path. Replace by RouteWithLink + self.tables[index][ch] = TablePointer::RouteWithLink(i, route); + } + _ => { + panic!("Not expected here, maybe duplicate") + } + } + } + + pub fn match_path<'a>(&self, path: &'a str) -> RouteMatch<'a, R> { + let mut table: &[TablePointer; 128] = &self.tables[0]; + let path_bytes = path.as_bytes(); + let path_max_i = path_bytes.len() - 1; + + let mut i = 0; + while i <= path_max_i { + let ch = path_bytes[i] as usize % 128; + match table[ch] { + TablePointer::NotFound => { + return Err(NoMatchingRoute); + } + TablePointer::Link(i_other_table) => { + table = &self.tables[i_other_table]; + i += 1; + } + TablePointer::RouteWithLink(i_other_table, route) => { + if i == path_max_i { + return Ok((route, Default::default())); + } else { + table = &self.tables[i_other_table]; + } + } + TablePointer::Route(route) => { + return Ok((route, path.get(i + 1..=path_max_i).unwrap())); + } + } + } + Err(NoMatchingRoute) + } +} + +#[cfg(test)] +mod test1 { + use super::*; + use test::Bencher; + + #[derive(Clone, Copy)] + enum Route { + Login, + Logout, + Info, + Check + } + + #[bench] + fn bench_1(b: &mut Bencher) { + let mut r = RoutingTable::new(); + r.insert("/login", Route::Login); + r.insert("/info", Route::Info); + r.insert("/logout", Route::Logout); + r.insert("/logout2", Route::Info); + r.insert("/check", Route::Check); + + match r.tables[0][47] { + TablePointer::Link(n) => assert!(n == 1), + _ => panic!("Wrong"), + } + + b.iter(|| { + match r.match_path("/login") { + RouteMatch::Match(Route::Login, rest) => assert_eq!(rest, ""), + _ => panic!("Wrong") + } + match r.match_path("/logout") { + RouteMatch::Match(Route::Logout, rest) => assert_eq!(rest, ""), + _ => panic!("Wrong") + } + match r.match_path("/info") { + RouteMatch::Match(Route::Info, rest) => assert_eq!(rest, ""), + _ => panic!("Wrong") + } + match r.match_path("/logout2") { + RouteMatch::Match(Route::Info, rest) => assert_eq!(rest, ""), + _ => panic!("Wrong") + } + match r.match_path("/asdasdasd") { + RouteMatch::None => (), + _ => panic!("Wrong") + } + + match r.match_path("/login/foo/bar") { + RouteMatch::Match(Route::Login, rest) => assert_eq!(rest, "/foo/bar"), + _ => panic!("Wrong") + } + }) + } +} \ No newline at end of file diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..8eba50c --- /dev/null +++ b/src/system.rs @@ -0,0 +1,8 @@ +use std::time; + +pub fn initialize_rng_from_time() { + let r = random::default(); + let now = time::SystemTime::now(); + let nano_secs = now.duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + r.seed([(nano_secs >> 64) as u64, nano_secs as u64]); +} diff --git a/src/totp.rs b/src/totp.rs new file mode 100644 index 0000000..09b4503 --- /dev/null +++ b/src/totp.rs @@ -0,0 +1,30 @@ +use oath::totp_custom_time; +use oath::HashType; +use std::time::{UNIX_EPOCH, SystemTime}; + +pub fn verify(secret: &str, token: &str) -> Result { + let time_step = 30; + let totp = |time| { + totp_custom_time(secret, 6, 0, time_step, time, &HashType::SHA512) + .map(|t| { + debug!("Generated OTP for probing {} for key {}", t, secret); + t + }) + .map(|t| format!("{:06}", t) == *token) + }; + let current_time: u64 = SystemTime::now().duration_since(UNIX_EPOCH) + .expect("Earlier than 1970-01-01 00:00:00 UTC").as_secs(); + if current_time % time_step <= 5 && totp(current_time - 30)? { + return Ok(true); + } + + if current_time % time_step >= 25 && totp(current_time + 30)? { + return Ok(true); + } + + if totp(current_time)? { + return Ok(true); + } + + Ok(false) +} \ No newline at end of file -- cgit v1.2.1