Compare commits

...

41 Commits

Author SHA1 Message Date
Drew Short a0f86b4435 Allow addresses to be parsed without ports 6 years ago
Drew Short 66dacfa710 Removed unused config reference 6 years ago
Drew Short da1a8e9e58 Fixing config values not being preferred over defaults. 6 years ago
Drew Short 8abb4b80d5 Updated the client to update a record instead of creating a new record. 6 years ago
Drew Short 580376f21b Making sure that the deployment only happens for master 6 years ago
Drew Short 0c1e6c8507 POC work for updating domain record automatically 6 years ago
Drew Short 7e69b9f29e Trying to standardize errors 6 years ago
Drew Short 9466cae494 Adding initial cloudflare client interaction 6 years ago
Drew Short 00e11e760e Cleaning up the endpoint auth middleware 6 years ago
Drew Short 71a12ccf22 Simplify the root/zone lookup 6 years ago
Drew Short 449b27747e import cleanup 6 years ago
Drew Short b68cdce121 Simple username+token authentication 6 years ago
Drew Short 076c11535c refactor following code from https://github.com/actix/examples/blob/master/simple-auth-server/ for app state 6 years ago
Drew Short 7fea659c6e Update config to mirror the api concept of root and zone 6 years ago
Drew Short 6e92cabcbe Cargo Clippy linting suggestions 6 years ago
Drew Short 3745d56e5a Ran cargo fix --edition and updated to use the rust 2018 parser 6 years ago
Drew Short bc6c72d136 Bumping the expected rust build containers to Rust 2018 (1.31) 6 years ago
Drew Short 0d2599fac7 ignoring the reported port for the moment 6 years ago
Drew Short f2a0b0520c Added support for parsing IP Addresses 6 years ago
Drew Short ae1fbd1d16 Added some windows scripts for building and deploying a docker container 6 years ago
Drew Short 52b9fcd5d7 Args update 6 years ago
Drew Short a4e2c2b46b Configuration file is not optional 6 years ago
Drew Short e44388501d Changed order of the default logging environment variable 6 years ago
Drew Short 17644a44bf Moved args into a separate module 6 years ago
Drew Short a6185c5d18 Resolved compilation and borrowing errors 6 years ago
Drew Short f95872126d Moved argument parsing into module 6 years ago
Drew Short 31cc0eb64e Missing assignment 6 years ago
Drew Short 69ca0b14dc Added some startup safety by not unwrapping actix_server directly 6 years ago
Drew Short 9cad1dfc91 Ran code formatting and cleaned imports. 6 years ago
Drew Short 97e273cb72 Moved config into a module 6 years ago
Drew Short 2acf50a700 Added user info to configuration file 6 years ago
Drew Short e3c106e438 Removed needless error passing from missing file error 6 years ago
Drew Short a9c0050ce6 Added address endpoint to print requester address 6 years ago
Drew Short 822311397d Additional logging for ConfigError 6 years ago
Drew Short e4f1c09ada Additional logging by default 6 years ago
Drew Short 99af21f322 Changed the updated cloudflare API to an open endpoint 6 years ago
Drew Short 85bd3b539b Dynamic defaults for workers and config setup from file or commandline 6 years ago
Drew Short 8b371675a5 Moved to serde_yaml for loading the config file 6 years ago
Drew Short 9dc05c9561 No reason to list patch numbers 6 years ago
Drew Short 739a28358b Updated dependencies 6 years ago
Drew Short 667e713a18 Working on loading from config file 6 years ago
  1. 3
      .gitignore
  2. 8
      .gitlab-ci.yml
  3. 1500
      Cargo.lock
  4. 24
      Cargo.toml
  5. 11
      docker/build-linux.bat
  6. 8
      docker/run-linux.bat
  7. 24
      rsddns-example.yml
  8. 39
      src/args/mod.rs
  9. 42
      src/args/parse.rs
  10. 46
      src/config/default.rs
  11. 30
      src/config/error.rs
  12. 33
      src/config/load.rs
  13. 5
      src/config/mod.rs
  14. 97
      src/config/model.rs
  15. 6
      src/config/validate.rs
  16. 125
      src/main.rs
  17. 216
      src/server/api.rs
  18. 59
      src/server/error.rs
  19. 62
      src/server/middleware/api_auth.rs
  20. 1
      src/server/middleware/mod.rs
  21. 10
      src/server/mod.rs
  22. 19
      src/server/router.rs
  23. 52
      src/server/util.rs

3
.gitignore

@ -1,2 +1,5 @@
/.idea
/docker/linux
/target
**/*.rs.bk
rsddns.yml

8
.gitlab-ci.yml

@ -4,7 +4,7 @@ stages:
- deploy
tests:
image: rust:1.29-stretch
image: rust:1.31-stretch
stage: test
script:
- cargo test
@ -12,7 +12,7 @@ tests:
- docker
debug:
image: rust:1.29-stretch
image: rust:1.31-stretch
stage: build
script:
- cargo build
@ -24,7 +24,7 @@ debug:
- docker
release:
image: rust:1.29-stretch
image: rust:1.31-stretch
stage: build
script:
- cargo build --release
@ -55,3 +55,5 @@ deploy:release:
- release
tags:
- linux
only:
- master

1500
Cargo.lock
File diff suppressed because it is too large
View File

24
Cargo.toml

@ -2,6 +2,7 @@
name = "rsddns"
version = "0.1.0"
authors = ["Drew Short <warrick@sothr.com>"]
edition = "2018"
[profile.release]
panic = "abort"
@ -10,14 +11,15 @@ codegen-units = 1
incremental = false
[dependencies]
bytes = "0.4.10"
clap = "2.32.0"
log = "0.4.0"
env_logger = "0.5.13"
num_cpus = "1.0"
yaml-rust = "0.4.0"
serde = "1.0.78"
serde_derive = "1.0.78"
futures = "0.1.24"
actix-web = "0.7.7"
cloudflare = { git = "https://github.com/nocduro/cloudflare-rs", branch = "master" }
bytes = "0.4"
clap = "2.32"
log = "0.4"
env_logger = "0.6"
num_cpus = "1.8"
serde = "1.0"
serde_derive = "1.0"
serde_yaml = "0.8"
futures = "0.1"
actix-web = "0.7"
lazy_static = "1.2.0"
cloudflare = { git = "https://gitlab.sothr.com:9142/github/cloudflare-rs.git", branch = "develop" }

11
docker/build-linux.bat

@ -0,0 +1,11 @@
if not exist "%cd%\docker\linux\target" mkdir "%cd%\docker\linux\target"
if not exist "%cd%\docker\linux\cargo" mkdir "%cd%\docker\linux\cargo"
docker run --rm ^
-e CARGO_HOME="/tmp/cargo" ^
-v "%cd%":/usr/src/rsddns ^
-v "%cd%\docker\linux\target":/usr/src/rsddns/target ^
-v "%cd%\docker\linux\cargo":/tmp/cargo ^
-w /usr/src/rsddns ^
rust:1.31-stretch ^
cargo build --release && strip /usr/src/rsddns/target/release/rsddns

8
docker/run-linux.bat

@ -0,0 +1,8 @@
copy rsddns.yml "%cd%\docker\linux\target\release\rsddns.yml"
docker run --rm -i -t ^
-p 8080:8080 ^
-v "%cd%\docker\linux\target\release":/opt/rsddns ^
-w /opt/rsddns ^
debian:stretch-slim ^
./rsddns -c ./rsddns.yml -h 0.0.0.0 -p 8080

24
rsddns-example.yml

@ -0,0 +1,24 @@
---
server:
host: localhost
port: 8080
workers: 4
cloudflare:
email: something@something.com
key: somekeyblahblahblahimakey
domains:
- domain: something.com
zone_id: blahblahchangemeimakey
ddns:
domains:
- domain: something.com
subdomains:
- ddns
users:
- username: userOne
token: 6d37d7a9-6b6b-4db2-99f2-c261e4f4b922
domains:
- domain: IAmNotADomain.com
root: ddns.IAmNotADomain.com
zones:
- home

39
src/args/mod.rs

@ -0,0 +1,39 @@
use clap::{App, Arg};
use crate::VERSION;
pub mod parse;
pub fn get_app() -> App<'static, 'static> {
App::new("Dynamic DNS Server")
.author("Drew Short, <warrick@sothr.com>")
.version(VERSION)
.about("Receive DDNS requests and update associated cloudflare subdomains")
.args(&[
Arg::with_name("config")
.short("c")
.long("config")
.value_name("PATH")
.default_value("/etc/rsddns/rsddns.yml")
.help("Set a custom configuration file path.")
.takes_value(true),
Arg::with_name("host")
.short("h")
.long("host")
.value_name("HOST")
.help("The address the server listens on.")
.takes_value(true),
Arg::with_name("port")
.short("p")
.long("port")
.value_name("PORT")
.help("The port to run the server on.")
.takes_value(true),
Arg::with_name("workers")
.short("w")
.long("workers")
.value_name("NUMBER")
.help("The number of workers to serve requests with (Defaults to the number of cores on the system).")
.takes_value(true),
])
}

42
src/args/parse.rs

@ -0,0 +1,42 @@
use std::str::FromStr;
use clap::ArgMatches;
use crate::config::model::Config;
fn get_config_for_string(arg_value: Option<&str>, config_value: &Option<String>, default_value: &str) -> String {
debug!("arg: {:?}; config: {:?}; default: {}", arg_value, config_value, default_value);
String::from(match arg_value {
Some(v) => v,
None => match config_value {
Some(host) => host,
None => default_value,
}
})
}
fn get_config_for_number<T: FromStr + std::fmt::Debug>(arg_value: Option<&str>, config_value: Option<T>, default_value: T) -> T {
debug!("arg: {:?}; config: {:?}; default: {:?}", arg_value, config_value, default_value);
match arg_value {
Some(v) => v.parse::<T>().unwrap_or(default_value),
None => match config_value {
Some(v) => v,
None => default_value
}
}
}
pub fn get_host(args: &ArgMatches, config: &Config, default_host: &str) -> String {
let arg_value = args.value_of("host");
get_config_for_string(arg_value, &config.server.host, default_host).clone()
}
pub fn get_port(args: &ArgMatches, config: &Config, default_port: i16) -> i16 {
let arg_value = args.value_of("port");
get_config_for_number(arg_value, config.server.port, default_port)
}
pub fn get_workers(args: &ArgMatches, config: &Config, default_workers: usize) -> usize {
let arg_value = args.value_of("workers");
get_config_for_number(arg_value, config.server.workers, default_workers)
}

46
src/config/default.rs

@ -0,0 +1,46 @@
use crate::config::model::*;
fn get_default_config() -> Config {
Config {
server: ServerConfig {
host: Option::Some(String::from("localhost")),
port: Option::Some(8080),
workers: Option::Some(4),
},
cloudflare: CloudflareConfig {
email: String::from("something@something.com"),
key: String::from("IAmNotAKey"),
domains: vec![
CloudflareDomainConfig {
domain: String::from("IAmNotADomain.com"),
zone_id: String::from("IAmNotAZoneID"),
}
],
},
ddns: DDNSConfig {
domains: vec![
DDNSDomain {
domain: String::from("IAmNotADomain.com"),
subdomains: vec![
String::from("ddns")
],
}
]
},
users: vec![
UserConfig {
username: String::from("userOne"),
token: String::from("6d37d7a9-6b6b-4db2-99f2-c261e4f4b922"),
roots: vec![
UserRootConfig {
domain: String::from("IAmNotADomain.com"),
root: String::from("ddns.IAmNotADomain.com"),
zones: vec![
String::from("home")
],
}
],
}
],
}
}

30
src/config/error.rs

@ -0,0 +1,30 @@
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct ConfigError {
description: String,
original_error: Option<Box<Error>>,
}
impl ConfigError {
pub fn new(description: &str, original_error: Option<Box<Error>>) -> ConfigError {
ConfigError {
description: String::from(description),
original_error,
}
}
}
impl Error for ConfigError {}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.original_error {
Some(original_error) => {
write!(f, "{}: \"{}\"", self.description, original_error)
}
None => write!(f, "{}", self.description)
}
}
}

33
src/config/load.rs

@ -0,0 +1,33 @@
use std::fs::File;
use std::io::prelude::*;
use serde_yaml;
use crate::config::error::ConfigError;
use crate::config::model::Config;
fn read_config(yaml_str: &str) -> Result<Config, ConfigError> {
match serde_yaml::from_str(yaml_str) {
Ok(v) => Result::Ok(v),
Err(e) => Result::Err(ConfigError::new("Invalid Configuration", Option::Some(Box::from(e))))
}
}
pub fn read(path: &str) -> Result<Config, ConfigError> {
match File::open(path) {
Ok(mut file) => {
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(c) => {
if c > 0 {
read_config(&contents)
} else {
Result::Err(ConfigError::new("Empty Configuration File", Option::None))
}
}
Err(e) => Result::Err(ConfigError::new("Cannot Read Configuration File", Option::Some(Box::from(e))))
}
}
Err(_e) => Result::Err(ConfigError::new(&format!("Configuration File Doesn't Exist \"{}\"", path), Option::None))
}
}

5
src/config/mod.rs

@ -0,0 +1,5 @@
pub mod default;
pub mod error;
pub mod load;
pub mod model;
pub mod validate;

97
src/config/model.rs

@ -0,0 +1,97 @@
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: Option<String>,
pub port: Option<i16>,
pub workers: Option<usize>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct CloudflareDomainConfig {
pub domain: String,
pub zone_id: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct CloudflareConfig {
pub domains: Vec<CloudflareDomainConfig>,
pub key: String,
pub email: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct DDNSDomain {
pub domain: String,
pub subdomains: Vec<String>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct DDNSConfig {
pub domains: Vec<DDNSDomain>
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct UserRootConfig {
pub domain: String,
pub root: String,
pub zones: Vec<String>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct UserConfig {
pub username: String,
pub token: String,
pub roots: Vec<UserRootConfig>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub server: ServerConfig,
pub cloudflare: CloudflareConfig,
pub ddns: DDNSConfig,
pub users: Vec<UserConfig>,
}
impl Config {
pub fn get_user(&self, username: &str) -> Option<&UserConfig> {
for user in &self.users {
if user.username == username {
return Some(user);
}
}
None
}
pub fn get_users_for_root_and_zone(&self, root: &str, zone: &str) -> Option<Vec<&UserConfig>> {
let mut users: Vec<&UserConfig> = Vec::new();
for user in &self.users {
if user.has_root_and_zone(root, zone) {
users.push(user);
}
}
if users.len() > 0 { Some(users) } else { None }
}
pub fn is_valid_username_and_token(&self, username: &str, token: &str) -> bool {
for user in &self.users {
if user.username == username && user.token == token {
return true;
}
}
return false;
}
}
impl UserConfig {
pub fn has_root_and_zone(&self, search_root: &str, search_zone: &str) -> bool {
for root in &self.roots {
let zone_match = &search_zone.to_string();
if root.root == search_root && root.zones.contains(zone_match) {
return true;
}
}
false
}
}

6
src/config/validate.rs

@ -0,0 +1,6 @@
use crate::config::model::Config;
use crate::config::error::ConfigError;
pub fn validate(config: &Config) -> Result<&Config, ConfigError> {
Ok(config)
}

125
src/main.rs

@ -1,84 +1,93 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate actix_web;
extern crate bytes;
extern crate clap;
extern crate cloudflare;
extern crate env_logger;
extern crate futures;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate num_cpus;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_yaml;
use std::process;
use std::sync::Arc;
use clap::{App, Arg};
use cloudflare::Cloudflare;
mod args;
mod config;
mod server;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_HOST: &str = "localhost";
const DEFAULT_PORT: i16 = 8080;
lazy_static! {
static ref DEFAULT_PORT_STR: String = DEFAULT_PORT.to_string();
static ref DEFAULT_WORKERS: usize = num_cpus::get();
static ref DEFAULT_WORKERS_STR: String = DEFAULT_WORKERS.to_string();
}
fn main() {
match std::env::var("RUST_LOG") {
Ok(_) => (),
Err(_) => std::env::set_var("RUST_LOG", "actix_web=info"),
Err(_) => std::env::set_var("RUST_LOG", "error,rsddns=info,actix_web=info")
}
env_logger::init();
let args = App::new("Dynamic DNS Server")
.version(VERSION)
.author("Drew Short <warrick@sothr.com>")
.about("Recieve DDNS requests and update cloudflare subdomains")
.args(&[
Arg::with_name("config")
.short("c")
.long("config")
.value_name("PATH")
.default_value("/etc/rsddns/rsddns.yml")
.help("Set a custom configuration file path.")
.takes_value(true),
Arg::with_name("host")
.short("h")
.long("host")
.value_name("HOST")
.default_value("localhost")
.help("The address the server listens on.")
.takes_value(true),
Arg::with_name("port")
.short("p")
.long("port")
.value_name("PORT")
.default_value("8080")
.help("The port to run the server on.")
.takes_value(true),
Arg::with_name("workers")
.short("w")
.long("workers")
.value_name("NUMBER")
.help("The number of workers to serve requests with.")
.takes_value(true),
])
.get_matches();
let args = args::get_app().get_matches();
let config: &str = args.value_of("config").unwrap_or("/etc/rsddns/rsddns.yml");
let host: &str = args.value_of("host").unwrap_or("localhost");
let port: i32 = args
.value_of("port")
.unwrap()
.parse::<i32>()
.unwrap_or(8080);
let workers: usize = match args.value_of("workers") {
Some(count) => count.parse::<usize>().unwrap_or(num_cpus::get()),
None => num_cpus::get(),
let config_path: &str = args.value_of("config").unwrap_or("/etc/rsddns/rsddns.yml");
let config = match config::load::read(config_path) {
Ok(c) => {
info!("Loaded configuration from \"{}\"", config_path);
c
}
Err(e) => {
error!("{}", e);
process::exit(-1)
}
};
match config::validate::validate(&config) {
Ok(_) => info!("Configuration Is Valid"),
Err(e) => {
error!("{}", e);
process::exit(-1)
}
}
let host = args::parse::get_host(&args, &config, DEFAULT_HOST);
let port = args::parse::get_port(&args, &config, DEFAULT_PORT);
let workers = args::parse::get_workers(&args, &config, *DEFAULT_WORKERS);
let bind = format!("{}:{}", host, port);
info!(
"Starting server on {}:{} with workers={} and config {}",
host, port, workers, config
"Starting server on {} with workers={} and config \"{}\"",
bind, workers, config_path
);
actix_web::server::new(|| server::router::create())
let shared_config = Arc::new(config);
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(format!("{}:{}", host, port))
.unwrap()
.run();
.bind(bind);
match actix_server {
Ok(server) => server.run(),
Err(e) => error!("{}", e)
}
}

216
src/server/api.rs

@ -1,13 +1,40 @@
use std::io::Read;
use std::net::IpAddr;
use std::str::FromStr;
use actix_web::{AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Scope};
use actix_web::http::Method;
use actix_web::http::StatusCode;
use actix_web::{AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Scope};
use bytes::{Buf, Bytes, IntoBuf};
use cloudflare::{Cloudflare, zones};
use futures::future::Future;
use std::io::Read;
pub fn route(scope: Scope<()>) -> Scope<()> {
scope.nested("{root}/{zone}", |zone_scope| {
use crate::config::model::Config;
use crate::server::error::{APIError, Error};
use crate::server::error::Result;
use crate::server::middleware::api_auth::APIAuthRootAndZone;
use crate::server::router::AppState;
use crate::server::util;
trait RecordType {
fn get_record_type(&self) -> zones::dns::RecordType;
}
impl RecordType for IpAddr {
fn get_record_type(&self) -> zones::dns::RecordType {
match &self {
IpAddr::V4(_) => zones::dns::RecordType::A,
IpAddr::V6(_) => zones::dns::RecordType::AAAA
}
}
}
pub fn route(scope: Scope<AppState>) -> Scope<AppState> {
scope
.resource("address", |r| r.method(Method::GET).f(get_request_address))
.nested("{root}/{zone}", |zone_scope| {
zone_scope
.middleware(APIAuthRootAndZone)
.resource("", |r| r.method(Method::GET).f(get_address))
.resource("update", |r| {
r.method(Method::GET).f(update_address_automatically);
@ -21,25 +48,190 @@ fn update_address(address: String) -> String {
address
}
fn get_address(req: &HttpRequest) -> HttpResponse {
fn parse_remote_info(remote_info: &str) -> Result<IpAddr> {
let mut remote_address = String::from(remote_info);
if remote_address.contains(':') {
let last_colon_index = remote_address.rfind(':').unwrap();
let _port = remote_address.split_off(last_colon_index);
if remote_address.starts_with('[') && remote_address.ends_with(']') {
remote_address = String::from(remote_address.trim_matches(|c| c == '[' || c == ']'))
}
match IpAddr::from_str(&remote_address) {
Ok(v) => Ok(v),
Err(e) => Err(APIError::new(&format!("Address Parse Error \"{}\"", remote_address), Some(Box::from(e))))
}
} else {
match IpAddr::from_str(&remote_address) {
Ok(v) => Ok(v),
Err(e) => Err(APIError::new(&format!("Address Parse Error \"{}\"", remote_address), Some(Box::from(e))))
}
}
}
fn determine_request_address(req: &HttpRequest<AppState>) -> Result<Option<IpAddr>> {
match req.connection_info().remote() {
Some(remote_info) => match parse_remote_info(remote_info) {
Ok(addr) => Ok(Some(addr)),
Err(e) => {
error!("{:?}", e);
Err(Error::from(e))
}
}
None => Ok(None),
}
}
fn get_request_address(req: &HttpRequest<AppState>) -> HttpResponse {
match determine_request_address(&req) {
Ok(potential_addr) => match potential_addr {
Some(addr) => HttpResponse::build(StatusCode::OK)
.content_type("text/plain")
.body(format!("{}", addr)),
None => HttpResponse::build(StatusCode::BAD_REQUEST).finish(),
None => HttpResponse::build(StatusCode::BAD_REQUEST).finish()
},
Err(e) => {
error!("{:?}", e);
HttpResponse::build(StatusCode::BAD_REQUEST).finish()
}
}
}
fn update_address_automatically(req: &HttpRequest) -> HttpResponse {
match req.connection_info().remote() {
Some(addr) => HttpResponse::build(StatusCode::OK)
fn find_dns_record(
cloudflare: &Cloudflare,
zone_id: &str,
record_type: zones::dns::RecordType,
record_name: &str,
) -> Result<Option<zones::dns::DnsRecord>> {
match zones::dns::list_dns_of_type(cloudflare, &zone_id, record_type) {
Ok(dns_entries) => {
debug!("Looking for dns record: {}", record_name);
for dns_entry in dns_entries {
debug!("record {:?}", dns_entry);
if dns_entry.name == record_name {
return Ok(Some(dns_entry));
}
}
Ok(None)
}
Err(e) => {
error!("{:?}", e);
Err(Error::from(e))
}
}
}
fn get_address(req: &HttpRequest<AppState>) -> HttpResponse {
let root_match = util::get_match_value(req, "root");
let zone_match = util::get_match_value(req, "zone");
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) => {
let zone_id = match zones::get_zoneid(cloudflare, &domain) {
Ok(zone_id) => zone_id,
Err(e) => {
error!("{:?}", e);
return HttpResponse::BadRequest().into();
}
};
match find_dns_record(cloudflare, &zone_id, zones::dns::RecordType::A, &format!("{}.{}", &zone, &root)) {
Ok(record) => match record {
Some(dns_entry) => HttpResponse::build(StatusCode::OK)
.content_type("text/plain")
.body(format!("{}", addr)),
None => HttpResponse::build(StatusCode::BAD_REQUEST).finish(),
.body(format!("{}", dns_entry.content)),
None => HttpResponse::NotFound().into()
},
Err(e) => {
error!("{:?}", e);
HttpResponse::InternalServerError().into()
}
}
},
None => HttpResponse::BadRequest().into()
}
}
}
fn update_address_automatically(req: &HttpRequest<AppState>) -> HttpResponse {
let root_match = util::get_match_value(req, "root");
let zone_match = util::get_match_value(req, "zone");
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();
let request_address = match determine_request_address(&req) {
Ok(potential_addr) => match potential_addr {
Some(addr) => addr,
None => return HttpResponse::build(StatusCode::BAD_REQUEST).finish()
},
Err(e) => {
error!("{:?}", e);
return HttpResponse::build(StatusCode::BAD_REQUEST).finish();
}
};
let request_address_content = format!("{}", request_address);
let record_name = format!("{}.{}", &zone, &root);
match util::get_domain_from_root(&root) {
Some(domain) => {
let zone_id = match zones::get_zoneid(cloudflare, &domain) {
Ok(zone_id) => zone_id,
Err(e) => {
error!("{:?}", e);
return HttpResponse::BadRequest().into();
}
};
match find_dns_record(cloudflare, &zone_id, request_address.get_record_type(), &record_name) {
Ok(record) => match record {
Some(dns_entry) => {
match zones::dns::update_dns_entry(
cloudflare,
&dns_entry.zone_id,
&dns_entry.id,
request_address.get_record_type(),
&dns_entry.name,
&request_address_content) {
Ok(result_record) => HttpResponse::build(StatusCode::OK)
.content_type("text/plain")
.body(format!("{}", result_record.content)),
Err(e) => {
error!("{:?}", e);
HttpResponse::InternalServerError().into()
}
}
}
None => match zones::dns::create_dns_entry(
cloudflare,
&zone_id,
request_address.get_record_type(),
&record_name,
&request_address_content) {
Ok(result_record) => HttpResponse::build(StatusCode::OK)
.content_type("text/plain")
.body(format!("{}", result_record.content)),
Err(e) => {
error!("{:?}", e);
HttpResponse::InternalServerError().into()
}
}
},
Err(e) => {
error!("{:?}", e);
HttpResponse::InternalServerError().into()
}
}
},
None => HttpResponse::BadRequest().into()
}
}
}
fn update_address_manually(req: &HttpRequest) -> FutureResponse<HttpResponse> {
fn update_address_manually(req: &HttpRequest<AppState>) -> FutureResponse<HttpResponse> {
req.body()
.limit(48)
.from_err()

59
src/server/error.rs

@ -0,0 +1,59 @@
use std::error;
use std::fmt;
use std::net;
use std::result;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
APIError(APIError),
Cloudflare(cloudflare::errors::Error),
AddrParseError(net::AddrParseError)
}
impl From<APIError> for Error {
fn from(err: APIError) -> Error {
Error::APIError(err)
}
}
impl From<cloudflare::errors::Error> for Error {
fn from(err: cloudflare::errors::Error) -> Error {
Error::Cloudflare(err)
}
}
impl From<net::AddrParseError> for Error {
fn from(err: net::AddrParseError) -> Error {
Error::AddrParseError(err)
}
}
#[derive(Debug)]
pub struct APIError {
description: String,
original_error: Option<Box<error::Error>>,
}
impl APIError {
pub fn new(description: &str, original_error: Option<Box<error::Error>>) -> Error {
Error::from(APIError {
description: String::from(description),
original_error,
})
}
}
impl error::Error for APIError {}
impl fmt::Display for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.original_error {
Some(original_error) => {
write!(f, "{}: \"{:?}\"", self.description, original_error)
}
None => write!(f, "{}", self.description)
}
}
}

62
src/server/middleware/api_auth.rs

@ -0,0 +1,62 @@
use actix_web::{HttpRequest, HttpResponse};
use actix_web::error::Result;
use actix_web::middleware::{Middleware, Started};
use crate::config::model::Config;
use crate::config::model::UserConfig;
use crate::server::router::AppState;
use crate::server::util;
fn valid_username_and_token_in_vec(username: &str, token: &str, users: Vec<&UserConfig>) -> bool {
for user in users {
if user.username == username && user.token == token {
return true;
}
}
return false;
}
pub struct APIAuthUser;
pub struct APIAuthRootAndZone;
impl Middleware<AppState> for APIAuthUser {
fn start(&self, req: &HttpRequest<AppState>) -> Result<Started> {
let config: &Config = &req.state().config;
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()) {
Ok(Started::Done)
} else {
Ok(Started::Response(HttpResponse::Unauthorized().into()))
}
}
}
impl Middleware<AppState> for APIAuthRootAndZone {
fn start(&self, req: &HttpRequest<AppState>) -> Result<Started> {
let config: &Config = &req.state().config;
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 = 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) {
Ok(Started::Done)
} else {
Ok(Started::Response(HttpResponse::Unauthorized().into()))
}
}
None => Ok(Started::Response(HttpResponse::Unauthorized().into()))
}
}
}
}

1
src/server/middleware/mod.rs

@ -0,0 +1 @@
pub mod api_auth;

10
src/server/mod.rs

@ -1,19 +1,23 @@
use actix_web::{HttpRequest, Json, Result};
use VERSION;
use crate::server::router::AppState;
use crate::VERSION;
pub mod api;
pub mod error;
pub mod middleware;
pub mod router;
pub mod util;
#[derive(Serialize)]
pub struct Health {
version: &'static str,
}
pub fn index(_req: &HttpRequest) -> &'static str {
pub fn index(_req: &HttpRequest<AppState>) -> &'static str {
"Hello, World!"
}
pub fn healthcheck(_req: HttpRequest) -> Result<Json<Health>> {
pub fn healthcheck(_req: HttpRequest<AppState>) -> Result<Json<Health>> {
Ok(Json(Health { version: VERSION }))
}

19
src/server/router.rs

@ -1,14 +1,23 @@
extern crate actix_web;
use std::sync::Arc;
use actix_web::{App, http};
use actix_web::middleware::Logger;
use actix_web::{http, App};
use cloudflare::Cloudflare;
use crate::config::model::Config;
use crate::server;
use server;
pub struct AppState {
pub config: Arc<Config>,
pub cloudflare: Arc<Cloudflare>
}
pub fn create() -> App {
actix_web::App::new()
pub fn create(config: Arc<Config>, cloudflare: Arc<Cloudflare>) -> App<AppState> {
actix_web::App::with_state(AppState { config, cloudflare })
.middleware(Logger::default())
.scope("api/", |api_scope| server::api::route(api_scope))
.scope("api/", server::api::route)
.resource("/health", |r| {
r.method(http::Method::GET).with(server::healthcheck)
})

52
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<S>(req: &HttpRequest<S>, key: &str) -> Option<String> {
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<S>(req: &HttpRequest<S>, key: &str) -> Option<String> {
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<AppState>) -> 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<S>(req: &HttpRequest<S>) -> Option<String> {
get_header_value(req, "X-AUTH-USERNAME")
}
pub fn get_token_from_request<S>(req: &HttpRequest<S>) -> Option<String> {
get_header_value(req, "X-AUTH-TOKEN")
}
pub fn get_domain_from_root(root: &str) -> Option<String> {
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(".")))
}
}
Loading…
Cancel
Save