Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add session creation endpoint #98

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion examples/simple.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use spaceapi_server::api;
use spaceapi_server::SpaceapiServerBuilder;
use spaceapi_server::{SpaceapiServerBuilder, UpdateSecurity};

fn main() {
// Create new minimal Status instance
Expand All @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion examples/with_sensors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
153 changes: 118 additions & 35 deletions src/server/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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",
))
}
}
}

Expand All @@ -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<Response> {
// 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::<Router>().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)))
}
}

Expand Down
55 changes: 50 additions & 5 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct SpaceapiServerBuilder {
redis_info: RedisInfo,
sensor_specs: Vec<sensors::SensorSpec>,
status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
update_security: UpdateSecurity,
}

impl SpaceapiServerBuilder {
Expand All @@ -57,6 +58,7 @@ impl SpaceapiServerBuilder {
redis_info: RedisInfo::None,
sensor_specs: vec![],
status_modifiers: vec![],
update_security: UpdateSecurity::HmacSha256,
}
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
})
}
}
Expand All @@ -170,6 +182,7 @@ pub struct SpaceapiServer {
redis_pool: RedisPool,
sensor_specs: sensors::SafeSensorSpecs,
status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
update_security: UpdateSecurity,
}

impl SpaceapiServer {
Expand All @@ -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
}
Expand All @@ -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,
}