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 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 {
|
||||||
|
|
210
src/main.rs
210
src/main.rs
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
|
@ -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