A cloudflare backed DDNS service written in Rust
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
9.5 KiB

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 bytes::{Buf, Bytes, IntoBuf};
use cloudflare::{Cloudflare, zones};
use futures::future::Future;
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);
r.method(Method::POST).f(update_address_manually)
})
})
}
fn update_address(address: String) -> String {
info!("Updating Address {}", address);
address
}
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()
},
Err(e) => {
error!("{:?}", e);
HttpResponse::build(StatusCode::BAD_REQUEST).finish()
}
}
}
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!("{}", 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<AppState>) -> FutureResponse<HttpResponse> {
req.body()
.limit(48)
.from_err()
.and_then(|bytes: Bytes| {
let mut buffer = String::new();
match bytes.into_buf().reader().read_to_string(&mut buffer) {
Ok(_) => Ok(HttpResponse::Ok()
.content_type("text/plain")
.body(update_address(buffer))),
Err(_) => Ok(HttpResponse::build(StatusCode::BAD_REQUEST).finish()),
}
})
.responder()
}