diff --git a/src/main.rs b/src/main.rs index 1cea0b1..1e3f704 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ extern crate actix_web; extern crate bytes; extern crate clap; +extern crate cloudflare; extern crate env_logger; extern crate futures; #[macro_use] @@ -16,6 +17,8 @@ extern crate serde_yaml; use std::process; use std::sync::Arc; +use cloudflare::Cloudflare; + mod args; mod config; mod server; @@ -71,7 +74,15 @@ fn main() { ); let shared_config = Arc::new(config); - let actix_server = actix_web::server::new(move || server::router::create(shared_config.clone())) + let shared_cloudflare = match Cloudflare::new( + &shared_config.cloudflare.key, + &shared_config.cloudflare.email, + "https://api.cloudflare.com/client/v4/") { + Ok(api) => Arc::new(api), + Err(_e) => process::exit(-1) + }; + + let actix_server = actix_web::server::new(move || server::router::create(shared_config.clone(), shared_cloudflare.clone())) .workers(workers) .bind(bind); diff --git a/src/server/api.rs b/src/server/api.rs index 4bd5668..26b76ae 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -6,15 +6,18 @@ use actix_web::{AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpRe use actix_web::http::Method; use actix_web::http::StatusCode; use bytes::{Buf, Bytes, IntoBuf}; +use cloudflare::{Cloudflare, zones}; use futures::future::Future; +use crate::config::model::Config; use crate::server::error::APIError; use crate::server::middleware::api_auth::APIAuthRootAndZone; use crate::server::router::AppState; +use crate::server::util; pub fn route(scope: Scope) -> Scope { scope - .resource("address", |r| r.method(Method::GET).f(get_address)) + .resource("address", |r| r.method(Method::GET).f(get_request_address)) .nested("{root}/{zone}", |zone_scope| { zone_scope .middleware(APIAuthRootAndZone) @@ -48,7 +51,7 @@ fn parse_remote_info(remote_info: &str) -> Result { } } -fn get_address(req: &HttpRequest) -> HttpResponse { +fn get_request_address(req: &HttpRequest) -> HttpResponse { match req.connection_info().remote() { Some(remote_info) => { match parse_remote_info(remote_info) { @@ -65,6 +68,47 @@ fn get_address(req: &HttpRequest) -> HttpResponse { } } +fn get_address(req: &HttpRequest) -> HttpResponse { + let root_match = util::get_match_value(req, "root"); + let zone_match = util::get_match_value(req, "zone"); + let config: &Config = &req.state().config; + let cloudflare: &Cloudflare = &req.state().cloudflare; + if root_match.is_none() || zone_match.is_none() { + HttpResponse::BadRequest().into() + } else { + let root = root_match.unwrap(); + let zone = zone_match.unwrap(); + match util::get_domain_from_root(&root) { + Some(domain) => match zones::get_zoneid(cloudflare, &domain) { + Ok(zone_id) => match zones::dns::list_dns_of_type(cloudflare, &zone_id, zones::dns::RecordType::A) { + Ok(dns_entries) => { + let record_name = format!("{}.{}", &zone, &root); + debug!("Looking for dns record: {}", record_name); + for dns_entry in dns_entries { + debug!("record {:?}", dns_entry); + if dns_entry.name == record_name { + return HttpResponse::build(StatusCode::OK) + .content_type("text/plain") + .body(format!("{}", dns_entry.content)); + } + } + HttpResponse::NotFound().into() + } + Err(e) => { + error!("{:?}", e); + HttpResponse::InternalServerError().into() + } + }, + Err(e) => { + error!("{:?}", e); + HttpResponse::InternalServerError().into() + } + }, + None => HttpResponse::BadRequest().into() + } + } +} + fn update_address_automatically(req: &HttpRequest) -> HttpResponse { match req.connection_info().remote() { Some(remote_info) => { diff --git a/src/server/middleware/api_auth.rs b/src/server/middleware/api_auth.rs index 1e8b858..d5c0503 100644 --- a/src/server/middleware/api_auth.rs +++ b/src/server/middleware/api_auth.rs @@ -5,41 +5,7 @@ use actix_web::middleware::{Middleware, Started}; use crate::config::model::Config; use crate::config::model::UserConfig; use crate::server::router::AppState; - -fn get_match_value(req: &HttpRequest, key: &str) -> Option { - let match_info = req.resource().match_info(); - match match_info.get(key) { - Some(value) => Some(String::from(value)), - None => None - } -} - -fn get_header_value(req: &HttpRequest, key: &str) -> Option { - match req.headers().get(key) { - Some(header) => match header.to_str() { - Ok(header_value) => Some(String::from(header_value)), - Err(_e) => None - }, - None => None - } -} - -fn get_user_from_request(req: &HttpRequest) -> Option<&UserConfig> { - let config: &Config = &req.state().config; - let username = get_username_from_request(req); - match username { - Some(username) => config.get_user(&username), - None => None - } -} - -fn get_username_from_request(req: &HttpRequest) -> Option { - get_header_value(req, "X-AUTH-USERNAME") -} - -fn get_token_from_request(req: &HttpRequest) -> Option { - get_header_value(req, "X-AUTH-TOKEN") -} +use crate::server::util; fn valid_username_and_token_in_vec(username: &str, token: &str, users: Vec<&UserConfig>) -> bool { for user in users { @@ -57,8 +23,8 @@ pub struct APIAuthRootAndZone; impl Middleware for APIAuthUser { fn start(&self, req: &HttpRequest) -> Result { let config: &Config = &req.state().config; - let username = get_username_from_request(req); - let token = get_token_from_request(req); + let username = util::get_username_from_request(req); + let token = util::get_token_from_request(req); if username.is_none() || token.is_none() { Ok(Started::Response(HttpResponse::Unauthorized().into())) } else if config.is_valid_username_and_token(&username.unwrap(), &token.unwrap()) { @@ -72,15 +38,15 @@ impl Middleware for APIAuthUser { impl Middleware for APIAuthRootAndZone { fn start(&self, req: &HttpRequest) -> Result { let config: &Config = &req.state().config; - let root = get_match_value(req, "root"); - let zone = get_match_value(req, "zone"); + let root = util::get_match_value(req, "root"); + let zone = util::get_match_value(req, "zone"); if root.is_none() || zone.is_none() { Ok(Started::Response(HttpResponse::BadRequest().into())) } else { match config.get_users_for_root_and_zone(&root.unwrap(), &zone.unwrap()) { Some(users) => { - let username = get_username_from_request(req); - let token = get_token_from_request(req); + let username = util::get_username_from_request(req); + let token = util::get_token_from_request(req); if username.is_none() || token.is_none() { Ok(Started::Response(HttpResponse::BadRequest().into())) } else if valid_username_and_token_in_vec(&username.unwrap(), &token.unwrap(), users) { diff --git a/src/server/mod.rs b/src/server/mod.rs index f7eec09..39a9d4f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -7,6 +7,7 @@ pub mod api; pub mod error; pub mod middleware; pub mod router; +pub mod util; #[derive(Serialize)] pub struct Health { diff --git a/src/server/router.rs b/src/server/router.rs index cb3acb9..f658cd6 100644 --- a/src/server/router.rs +++ b/src/server/router.rs @@ -4,16 +4,18 @@ use std::sync::Arc; use actix_web::{App, http}; use actix_web::middleware::Logger; +use cloudflare::Cloudflare; use crate::config::model::Config; use crate::server; pub struct AppState { - pub config: Arc + pub config: Arc, + pub cloudflare: Arc } -pub fn create(config: Arc) -> App { - actix_web::App::with_state(AppState { config }) +pub fn create(config: Arc, cloudflare: Arc) -> App { + actix_web::App::with_state(AppState { config, cloudflare }) .middleware(Logger::default()) .scope("api/", server::api::route) .resource("/health", |r| { diff --git a/src/server/util.rs b/src/server/util.rs new file mode 100644 index 0000000..67801b3 --- /dev/null +++ b/src/server/util.rs @@ -0,0 +1,52 @@ +use actix_web::HttpRequest; + +use crate::config::model::Config; +use crate::config::model::UserConfig; +use crate::server::router::AppState; + +pub fn get_match_value(req: &HttpRequest, key: &str) -> Option { + let match_info = req.resource().match_info(); + match match_info.get(key) { + Some(value) => Some(String::from(value)), + None => None + } +} + +pub fn get_header_value(req: &HttpRequest, key: &str) -> Option { + match req.headers().get(key) { + Some(header) => match header.to_str() { + Ok(header_value) => Some(String::from(header_value)), + Err(_e) => None + }, + None => None + } +} + +pub fn get_user_from_request(req: &HttpRequest) -> Option<&UserConfig> { + let config: &Config = &req.state().config; + let username = get_username_from_request(req); + match username { + Some(username) => config.get_user(&username), + None => None + } +} + +pub fn get_username_from_request(req: &HttpRequest) -> Option { + get_header_value(req, "X-AUTH-USERNAME") +} + +pub fn get_token_from_request(req: &HttpRequest) -> Option { + get_header_value(req, "X-AUTH-TOKEN") +} + +pub fn get_domain_from_root(root: &str) -> Option { + let mut parts: Vec<&str> = root.split('.').collect(); + let length = parts.len(); + if length <= 1 { + None + } else if length == 2 { + Some(String::from(root)) + } else { + Some(String::from(parts.split_off(length - 2).join("."))) + } +} \ No newline at end of file