Break out web server to standalone module.

This commit is contained in:
Dorian 2018-07-27 16:26:50 -04:00
parent ff0b1067bc
commit 50472613d9
3 changed files with 216 additions and 217 deletions

View File

@ -1,18 +1,15 @@
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate docopt;
extern crate actix_web; extern crate actix_web;
extern crate reqwest;
extern crate crypto;
extern crate env_logger;
extern crate log;
extern crate chrono; extern crate chrono;
extern crate crypto;
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
#[macro_use] extern crate log;
extern crate reqwest;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
pub mod git; pub mod git;
pub mod security; pub mod security;
pub mod web;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct UserProfile { pub struct UserProfile {

View File

@ -9,20 +9,9 @@ extern crate env_logger;
extern crate rookeries; extern crate rookeries;
use std::vec::Vec; use crypto::pbkdf2::{pbkdf2_simple};
use docopt::Docopt; use docopt::Docopt;
use actix_web::{server, App, HttpRequest, Responder, Json, HttpResponse}; use rookeries::web::run_server;
use actix_web::error::Result as ErrorResult;
use actix_web::middleware::{ErrorHandlers, Logger, Response};
use actix_web::http;
use reqwest::{Client, StatusCode};
use crypto::pbkdf2::{pbkdf2_check, pbkdf2_simple};
use rookeries::User;
use rookeries::git::get_git_commit_version;
use rookeries::security::JwtToken;
const USAGE: &'static str = " const USAGE: &'static str = "
@ -47,54 +36,6 @@ struct Args {
arg_pass: String, arg_pass: String,
} }
#[derive(Serialize)]
struct Status {
version: String,
app: String,
#[serde(rename = "gitRevision")]
git: String,
}
#[derive(Serialize)]
struct ErrorMessage {
error: ErrorDetails,
}
#[derive(Serialize)]
struct ErrorDetails {
message: String,
status_code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<String>,
}
#[derive(Deserialize)]
struct UserCredentialsRequest {
username: String,
password: String,
}
#[derive(Serialize)]
struct QueryOnUsername {
username: String,
}
#[derive(Serialize)]
struct CouchDBQuery<T> {
selector: T,
}
#[derive(Serialize, Deserialize)]
struct CouchDBUserResult {
docs: Vec<User>,
}
#[derive(Serialize)]
struct JwtTokenResponse {
access_token: String,
user: User,
}
fn main() { fn main() {
env_logger::init(); env_logger::init();
@ -128,150 +69,3 @@ fn get_app_version() -> String {
authors = env!("CARGO_PKG_AUTHORS"), authors = env!("CARGO_PKG_AUTHORS"),
).to_string() ).to_string()
} }
fn status(_req: HttpRequest) -> impl Responder {
let git_revision = match get_git_commit_version() {
Ok(git_info) => git_info,
Err(err) => {
error!("Unable to get git revision because of: {}", err);
"unknown".to_string()
}
};
Json(Status {
version: env!("CARGO_PKG_VERSION").to_string(),
app: env!("CARGO_PKG_NAME").to_string(),
git: git_revision,
})
}
fn auth(credentials: Json<UserCredentialsRequest>) -> impl Responder {
let db_connection = match std::env::var("ROOKERIES_COUCHDB") {
Ok(db_uri) => db_uri,
Err(_) => {
error!("Missing ROOKERIES_COUCHDB");
return json_error_message(
StatusCode::InternalServerError,
"Server not configured correctly",
);
}
};
// TODO: Consider wrapping everything into a CouchDB client of sorts.
// TODO: Clean up this entire thing.
info!("Attempting to authenticate '{}'", &credentials.username);
let client = Client::new();
let couchdb_uri = format!("{}/_find/", db_connection);
let query = CouchDBQuery { selector: QueryOnUsername {username: credentials.username.clone()} };
let post_request = client.post(&couchdb_uri)
// TODO: Get credentials from environment variables.
.basic_auth("admin", Some("password"))
.json(&query)
.send();
if post_request.is_err() {
error!("request error: {}", post_request.unwrap_err());
return json_error_message(
StatusCode::InternalServerError,
"An error occurred while calling the database.",
);
}
let mut response = post_request.unwrap();
if response.status() != StatusCode::Ok {
error!("request error: {}", &response.status());
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
};
let results: CouchDBUserResult = response.json().unwrap();
if results.docs.len() == 0 {
info!("User '{}' could not be found.", &credentials.username);
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
}
let user = &results.docs[0];
let stored_password = &user.password.clone();
let is_password_valid = match pbkdf2_check(&credentials.password, stored_password) {
Ok(valid) => valid,
Err(err_message) => {
warn!("Unable to check password because of: {}", err_message);
false
}
};
if is_password_valid == false {
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
}
let auth_secret = match std::env::var("ROOKERIES_JWT_SECRET_KEY") {
Ok(db_uri) => db_uri,
Err(_) => {
error!("Missing ROOKERIES_JWT_SECRET_KEY");
return json_error_message(
StatusCode::InternalServerError,
"Server not configured correctly",
);
}
};
let access_token_response = JwtTokenResponse {
access_token: JwtToken::new(user).encode(auth_secret),
user: user.clone()
};
let status_code = http::StatusCode::from_u16(StatusCode::Ok.as_u16()).unwrap();
HttpResponse::build(status_code).json(access_token_response)
}
fn json_error_message(status: StatusCode, message: &str) -> HttpResponse {
json_error_message_with_resource(status, message, None)
}
fn json_error_message_with_resource(status: StatusCode, message: &str, resource: Option<String>) -> HttpResponse {
let error_message = ErrorMessage {
error: ErrorDetails {
message: message.to_string(),
status_code: status.as_u16(),
resource,
}
};
let status_code = http::StatusCode::from_u16(status.as_u16()).unwrap();
HttpResponse::build(status_code).json(error_message)
}
fn render_missing_resource<S>(req: &mut HttpRequest<S>, _resp: HttpResponse) -> ErrorResult<Response> {
if req.method() != http::Method::GET {
return render_invalid_method_used(req, _resp);
}
let error_response = json_error_message_with_resource(StatusCode::NotFound, "Resource not found.", Some(req.path().to_string()));
Ok(Response::Done(error_response))
}
fn render_invalid_method_used<S>(_req: &mut HttpRequest<S>, _resp: HttpResponse) -> ErrorResult<Response> {
let error_response = json_error_message(StatusCode::MethodNotAllowed, "Specified method is invalid for this resource");
Ok(Response::Done(error_response))
}
fn run_server(port: u32) {
info!("Rookeries API Server - v{}", env!("CARGO_PKG_VERSION"));
info!("Starting server...");
server::new(|| {
App::new()
.middleware(Logger::new("%a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T"))
.middleware(ErrorHandlers::new()
.handler(http::StatusCode::NOT_FOUND, render_missing_resource)
.handler(http::StatusCode::METHOD_NOT_ALLOWED, render_invalid_method_used)
)
.resource("/status", |r| r.get().f(status))
.resource("/auth", |r| r.post().with(auth))
})
.bind(format!("0.0.0.0:{}", port))
.unwrap()
.run();
}

208
src/web.rs Normal file
View File

@ -0,0 +1,208 @@
use actix_web::{server, App, HttpRequest, Responder, Json, HttpResponse};
use actix_web::error::Result as ErrorResult;
use actix_web::middleware::{ErrorHandlers, Logger, Response};
use actix_web::http;
use reqwest::{Client, StatusCode};
use crypto::pbkdf2::{pbkdf2_check};
use std::env;
use User;
use git::get_git_commit_version;
use security::JwtToken;
#[derive(Serialize)]
struct Status {
version: String,
app: String,
#[serde(rename = "gitRevision")]
git: String,
}
#[derive(Serialize)]
struct ErrorMessage {
error: ErrorDetails,
}
#[derive(Serialize)]
struct ErrorDetails {
message: String,
status_code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<String>,
}
#[derive(Deserialize)]
struct UserCredentialsRequest {
username: String,
password: String,
}
#[derive(Serialize)]
struct QueryOnUsername {
username: String,
}
#[derive(Serialize)]
struct CouchDBQuery<T> {
selector: T,
}
#[derive(Serialize, Deserialize)]
struct CouchDBUserResult {
docs: Vec<User>,
}
#[derive(Serialize)]
struct JwtTokenResponse {
access_token: String,
user: User,
}
fn status(_req: HttpRequest) -> impl Responder {
let git_revision = match get_git_commit_version() {
Ok(git_info) => git_info,
Err(err) => {
error!("Unable to get git revision because of: {}", err);
"unknown".to_string()
}
};
Json(Status {
version: env!("CARGO_PKG_VERSION").to_string(),
app: env!("CARGO_PKG_NAME").to_string(),
git: git_revision,
})
}
fn auth(credentials: Json<UserCredentialsRequest>) -> impl Responder {
let db_connection = match env::var("ROOKERIES_COUCHDB") {
Ok(db_uri) => db_uri,
Err(_) => {
error!("Missing ROOKERIES_COUCHDB");
return json_error_message(
StatusCode::InternalServerError,
"Server not configured correctly",
);
}
};
// TODO: Consider wrapping everything into a CouchDB client of sorts.
// TODO: Clean up this entire thing.
info!("Attempting to authenticate '{}'", &credentials.username);
let client = Client::new();
let couchdb_uri = format!("{}/_find/", db_connection);
let query = CouchDBQuery { selector: QueryOnUsername {username: credentials.username.clone()} };
let post_request = client.post(&couchdb_uri)
// TODO: Get credentials from environment variables.
.basic_auth("admin", Some("password"))
.json(&query)
.send();
if post_request.is_err() {
error!("request error: {}", post_request.unwrap_err());
return json_error_message(
StatusCode::InternalServerError,
"An error occurred while calling the database.",
);
}
let mut response = post_request.unwrap();
if response.status() != StatusCode::Ok {
error!("request error: {}", &response.status());
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
};
let results: CouchDBUserResult = response.json().unwrap();
if results.docs.len() == 0 {
info!("User '{}' could not be found.", &credentials.username);
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
}
let user = &results.docs[0];
let stored_password = &user.password.clone();
let is_password_valid = match pbkdf2_check(&credentials.password, stored_password) {
Ok(valid) => valid,
Err(err_message) => {
warn!("Unable to check password because of: {}", err_message);
false
}
};
if is_password_valid == false {
return json_error_message(StatusCode::Unauthorized, "Invalid credentials provided.");
}
let auth_secret = match env::var("ROOKERIES_JWT_SECRET_KEY") {
Ok(db_uri) => db_uri,
Err(_) => {
error!("Missing ROOKERIES_JWT_SECRET_KEY");
return json_error_message(
StatusCode::InternalServerError,
"Server not configured correctly",
);
}
};
let access_token_response = JwtTokenResponse {
access_token: JwtToken::new(user).encode(auth_secret),
user: user.clone()
};
let status_code = http::StatusCode::from_u16(StatusCode::Ok.as_u16()).unwrap();
HttpResponse::build(status_code).json(access_token_response)
}
fn json_error_message(status: StatusCode, message: &str) -> HttpResponse {
json_error_message_with_resource(status, message, None)
}
fn json_error_message_with_resource(status: StatusCode, message: &str, resource: Option<String>) -> HttpResponse {
let error_message = ErrorMessage {
error: ErrorDetails {
message: message.to_string(),
status_code: status.as_u16(),
resource,
}
};
let status_code = http::StatusCode::from_u16(status.as_u16()).unwrap();
HttpResponse::build(status_code).json(error_message)
}
fn render_missing_resource<S>(req: &mut HttpRequest<S>, _resp: HttpResponse) -> ErrorResult<Response> {
if req.method() != http::Method::GET {
return render_invalid_method_used(req, _resp);
}
let error_response = json_error_message_with_resource(StatusCode::NotFound, "Resource not found.", Some(req.path().to_string()));
Ok(Response::Done(error_response))
}
fn render_invalid_method_used<S>(_req: &mut HttpRequest<S>, _resp: HttpResponse) -> ErrorResult<Response> {
let error_response = json_error_message(StatusCode::MethodNotAllowed, "Specified method is invalid for this resource");
Ok(Response::Done(error_response))
}
pub fn run_server(port: u32) {
info!("Rookeries API Server - v{}", env!("CARGO_PKG_VERSION"));
info!("Starting server...");
server::new(|| {
App::new()
.middleware(Logger::new("%a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T"))
.middleware(ErrorHandlers::new()
.handler(http::StatusCode::NOT_FOUND, render_missing_resource)
.handler(http::StatusCode::METHOD_NOT_ALLOWED, render_invalid_method_used)
)
.resource("/status", |r| r.get().f(status))
.resource("/auth", |r| r.post().with(auth))
})
.bind(format!("0.0.0.0:{}", port))
.unwrap()
.run();
}