Break out web server to standalone module.
This commit is contained in:
parent
ff0b1067bc
commit
50472613d9
15
src/lib.rs
15
src/lib.rs
|
@ -1,18 +1,15 @@
|
|||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate docopt;
|
||||
extern crate actix_web;
|
||||
extern crate reqwest;
|
||||
extern crate crypto;
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
|
||||
extern crate chrono;
|
||||
extern crate crypto;
|
||||
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 security;
|
||||
pub mod web;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct UserProfile {
|
||||
|
|
210
src/main.rs
210
src/main.rs
|
@ -9,20 +9,9 @@ extern crate env_logger;
|
|||
|
||||
extern crate rookeries;
|
||||
|
||||
use std::vec::Vec;
|
||||
|
||||
use crypto::pbkdf2::{pbkdf2_simple};
|
||||
use docopt::Docopt;
|
||||
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, pbkdf2_simple};
|
||||
|
||||
use rookeries::User;
|
||||
use rookeries::git::get_git_commit_version;
|
||||
use rookeries::security::JwtToken;
|
||||
use rookeries::web::run_server;
|
||||
|
||||
const USAGE: &'static str = "
|
||||
|
||||
|
@ -47,54 +36,6 @@ struct Args {
|
|||
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() {
|
||||
env_logger::init();
|
||||
|
||||
|
@ -128,150 +69,3 @@ fn get_app_version() -> String {
|
|||
authors = env!("CARGO_PKG_AUTHORS"),
|
||||
).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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
Loading…
Reference in New Issue