diff --git a/Cargo.toml b/Cargo.toml index e5bb16afd..840c2c834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ redis = "^0.8" log = "^0.4" iron = "^0.6.1" urlencoded = "^0.6" +rand = "0.7" router = "^0.6" serde = "^1.0" serde_json = "^1.0" diff --git a/examples/simple.rs b/examples/simple.rs index e988b4259..d2262fd0a 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,5 +1,5 @@ use spaceapi_server::api; -use spaceapi_server::SpaceapiServerBuilder; +use spaceapi_server::{SpaceapiServerBuilder, UpdateSecurity}; fn main() { // Create new minimal Status instance @@ -24,6 +24,7 @@ fn main() { // Set up server let server = SpaceapiServerBuilder::new(status) .redis_connection_info("redis://127.0.0.1/") + .with_update_security_mode(UpdateSecurity::NoUpdates) .build() .unwrap(); diff --git a/examples/with_sensors.rs b/examples/with_sensors.rs index 95e49261b..ee3846e99 100644 --- a/examples/with_sensors.rs +++ b/examples/with_sensors.rs @@ -2,7 +2,7 @@ use env_logger; use spaceapi_server::api; use spaceapi_server::api::sensors::{PeopleNowPresentSensorTemplate, TemperatureSensorTemplate}; use spaceapi_server::modifiers::StateFromPeopleNowPresent; -use spaceapi_server::SpaceapiServerBuilder; +use spaceapi_server::{SpaceapiServerBuilder, UpdateSecurity}; fn main() { env_logger::init(); @@ -29,6 +29,7 @@ fn main() { // Set up server let server = SpaceapiServerBuilder::new(status) .redis_connection_info("redis://127.0.0.1/") + .with_update_security_mode(UpdateSecurity::Insecure) .add_status_modifier(StateFromPeopleNowPresent) .add_sensor( PeopleNowPresentSensorTemplate { diff --git a/src/lib.rs b/src/lib.rs index 30ca1939b..0c8a3a0a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,6 +204,7 @@ mod types; pub use crate::errors::SpaceapiServerError; pub use crate::server::SpaceapiServer; pub use crate::server::SpaceapiServerBuilder; +pub use crate::server::UpdateSecurity; /// Return own crate version. Used in API responses. pub fn get_version() -> &'static str { diff --git a/src/server/handlers.rs b/src/server/handlers.rs index 1ce00bc3c..069c21638 100644 --- a/src/server/handlers.rs +++ b/src/server/handlers.rs @@ -4,6 +4,9 @@ use iron::modifiers::Header; use iron::prelude::*; use iron::{headers, middleware, status}; use log::{debug, error, info, warn}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use redis::Commands; use router::Router; use serde::ser::{Serialize, SerializeMap, Serializer}; use serde_json; @@ -15,6 +18,8 @@ use crate::modifiers; use crate::sensors; use crate::types::RedisPool; +const SESSION_VALIDITY_S: usize = 60; + #[derive(Debug)] struct ErrorResponse { reason: String, @@ -32,6 +37,23 @@ impl Serialize for ErrorResponse { } } +/// Build an error response with the specified `error_code` and the specified `reason` text. +fn err_response(error_code: status::Status, reason: &str) -> Response { + let error = ErrorResponse { + reason: reason.into(), + }; + let error_string = serde_json::to_string(&error).expect("Could not serialize error"); + Response::with((error_code, error_string)) + // Set headers + .set(Header(headers::ContentType( + "application/json; charset=utf-8".parse().unwrap(), + ))) + .set(Header(headers::CacheControl(vec![ + headers::CacheDirective::NoCache, + ]))) + .set(Header(headers::AccessControlAllowOrigin::Any)) +} + pub(crate) struct ReadHandler { status: api::Status, redis_pool: RedisPool, @@ -149,36 +171,6 @@ impl UpdateHandler { // Store data sensor_spec.set_sensor_value(&self.redis_pool, value) } - - /// Build an OK response with the `HTTP 204 No Content` status code. - fn ok_response(&self) -> Response { - Response::with(status::NoContent) - // Set headers - .set(Header(headers::ContentType( - "application/json; charset=utf-8".parse().unwrap(), - ))) - .set(Header(headers::CacheControl(vec![ - headers::CacheDirective::NoCache, - ]))) - .set(Header(headers::AccessControlAllowOrigin::Any)) - } - - /// Build an error response with the specified `error_code` and the specified `reason` text. - fn err_response(&self, error_code: status::Status, reason: &str) -> Response { - let error = ErrorResponse { - reason: reason.into(), - }; - let error_string = serde_json::to_string(&error).expect("Could not serialize error"); - Response::with((error_code, error_string)) - // Set headers - .set(Header(headers::ContentType( - "application/json; charset=utf-8".parse().unwrap(), - ))) - .set(Header(headers::CacheControl(vec![ - headers::CacheDirective::NoCache, - ]))) - .set(Header(headers::AccessControlAllowOrigin::Any)) - } } impl middleware::Handler for UpdateHandler { @@ -202,9 +194,14 @@ impl middleware::Handler for UpdateHandler { sensor_value = match params.get("value") { Some(ref values) => match values.len() { 1 => values[0].to_string(), - _ => return Ok(self.err_response(status::BadRequest, "Too many values specified")), + _ => return Ok(err_response(status::BadRequest, "Too many values specified")), }, - None => return Ok(self.err_response(status::BadRequest, "\"value\" parameter not specified")), + None => { + return Ok(err_response( + status::BadRequest, + "\"value\" parameter not specified", + )) + } } } @@ -216,17 +213,103 @@ impl middleware::Handler for UpdateHandler { ); let response = match e { sensors::SensorError::UnknownSensor(sensor) => { - self.err_response(status::BadRequest, &format!("Unknown sensor: {}", sensor)) + err_response(status::BadRequest, &format!("Unknown sensor: {}", sensor)) } sensors::SensorError::Redis(_) | sensors::SensorError::R2d2(_) => { - self.err_response(status::InternalServerError, "Updating values in datastore failed") + err_response(status::InternalServerError, "Updating values in datastore failed") } }; return Ok(response); }; // Create response - Ok(self.ok_response()) + Ok(Response::with(status::NoContent) + // Set headers + .set(Header(headers::ContentType( + "application/json; charset=utf-8".parse().unwrap(), + ))) + .set(Header(headers::CacheControl(vec![ + headers::CacheDirective::NoCache, + ]))) + .set(Header(headers::AccessControlAllowOrigin::Any))) + } +} + +pub(crate) struct CreateSessionHandler { + redis_pool: RedisPool, + sensor_specs: sensors::SafeSensorSpecs, +} + +impl CreateSessionHandler { + pub(crate) fn new(redis_pool: RedisPool, sensor_specs: sensors::SafeSensorSpecs) -> Self { + Self { + redis_pool, + sensor_specs, + } + } + + fn create_random_token(&self) -> String { + thread_rng().sample_iter(&Alphanumeric).take(12).collect() + } +} + +impl middleware::Handler for CreateSessionHandler { + /// Create a new session. + fn handle(&self, req: &mut Request) -> IronResult { + // TODO: create macro for these info! invocations. + info!("{} /{} from {}", req.method, req.url.path()[0], req.remote_addr); + + // Get sensor name + // TODO: Properly propagate errors + let params = req.extensions.get::().unwrap(); + let sensor_name = params.find("sensor").unwrap().to_string(); + + // Validate sensor + if !self + .sensor_specs + .iter() + .any(|ref spec| spec.data_key == sensor_name) + { + return Ok(err_response( + status::BadRequest, + &format!("Unknown sensor: {}", sensor_name), + )); + } + + // Create token + let token = self.create_random_token(); + + // Create session + let conn = match self.redis_pool.get() { + Ok(conn) => conn, + Err(e) => { + error!("Could not get redis connection: {}", e); + return Ok(err_response( + status::InternalServerError, + "Could not get redis connection", + )); + } + }; + let key = format!("{}.session.{}", sensor_name, token); + match conn.set_ex(&key, "active_session", SESSION_VALIDITY_S) { + Ok(()) => {} + Err(e) => { + error!("Could not create session: {}", e); + return Ok(err_response( + status::InternalServerError, + "Could not create session in database", + )); + } + }; + + // Return token + Ok(Response::with((status::Created, token)) + // Set headers + .set(Header(headers::ContentType("text/plain".parse().unwrap()))) + .set(Header(headers::CacheControl(vec![ + headers::CacheDirective::NoCache, + ]))) + .set(Header(headers::AccessControlAllowOrigin::Any))) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 5f0ba022b..46faee800 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -37,6 +37,7 @@ pub struct SpaceapiServerBuilder { redis_info: RedisInfo, sensor_specs: Vec, status_modifiers: Vec>, + update_security: UpdateSecurity, } impl SpaceapiServerBuilder { @@ -57,6 +58,7 @@ impl SpaceapiServerBuilder { redis_info: RedisInfo::None, sensor_specs: vec![], status_modifiers: vec![], + update_security: UpdateSecurity::HmacSha256, } } @@ -116,6 +118,15 @@ impl SpaceapiServerBuilder { self } + /// Use a certain update security mode for the sensor values. + /// + /// See [`UpdateSecurity`](enum.UpdateSecurity.html) for more details. By + /// default, `HmacSha256` will be used. + pub fn with_update_security_mode(mut self, mode: UpdateSecurity) -> Self { + self.update_security = mode; + self + } + /// Build a server instance. /// /// This can fail if not all required data has been provided. @@ -154,6 +165,7 @@ impl SpaceapiServerBuilder { redis_pool: pool?, sensor_specs: Arc::new(self.sensor_specs), status_modifiers: self.status_modifiers, + update_security: self.update_security, }) } } @@ -170,6 +182,7 @@ pub struct SpaceapiServer { redis_pool: RedisPool, sensor_specs: sensors::SafeSensorSpecs, status_modifiers: Vec>, + update_security: UpdateSecurity, } impl SpaceapiServer { @@ -188,11 +201,25 @@ impl SpaceapiServer { "root", ); - router.put( - "/sensors/:sensor/", - handlers::UpdateHandler::new(self.redis_pool.clone(), self.sensor_specs.clone()), - "sensors", - ); + // Add route to update sensor values + if let UpdateSecurity::NoUpdates = self.update_security { + // No route needed + } else { + router.put( + "/sensors/:sensor/", + handlers::UpdateHandler::new(self.redis_pool.clone(), self.sensor_specs.clone()), + "sensors", + ); + } + + // Add route to create session + if let UpdateSecurity::HmacSha256 = self.update_security { + router.post( + "/sensors/:sensor/sessions/", + handlers::CreateSessionHandler::new(self.redis_pool.clone(), self.sensor_specs.clone()), + "sessions", + ); + } router } @@ -212,3 +239,21 @@ impl SpaceapiServer { Iron::new(router).http(socket_addr) } } + +/// The security mode used to update sensor values dynamically. +/// +/// If you don't want to update sensor values through spaceapi-server-rs, +/// choose `NoUpdates` which disables updates completely. +/// +/// The recommended variant is `HmacSha256`. +pub enum UpdateSecurity { + /// No authentication. Anybody can update sensor values. + Insecure, + /// Static auth token. Can be sniffed by anybody if connection is not + /// encrypted. Vulnerable to replay attacks. + StaticToken, + /// Session based HMAC-SHA256 signatures. This is the recommended mode. + HmacSha256, + /// Disallow updates through the API. + NoUpdates, +}