use std::convert::TryInto; use serde_json::json; use rocket::Response; use rocket::http::Status; use rocket_contrib::json::Json; use trust_dns_client::{client::ClientHandle, op::UpdateMessage}; use trust_dns_client::op::ResponseCode; use trust_dns_client::rr::{DNSClass, RecordType}; use trust_dns_proto::DnsHandle; pub use trust_dns_client::op::Message; pub use trust_dns_client::op::OpCode; pub use trust_dns_client::op::Query; pub use trust_dns_client::op::MessageType; use crate::{DbConn, models::{dns, trust_dns_types}}; use crate::models::errors::{ErrorResponse, make_500}; use crate::models::users::{LocalUser, UserInfo, Zone, AddZoneMemberRequest, CreateZoneRequest}; #[get("/zones//records")] pub async fn get_zone_records( mut client: dns::DnsClient, conn: DbConn, user_info: Result, zone: dns::AbsoluteName ) -> Result>, ErrorResponse> { let user_info = user_info?; let zone_name = zone.to_string(); conn.run(move |c| { if user_info.is_admin() { Zone::get_by_name(c, &zone_name) } else { user_info.get_zone(c, &zone_name) } }).await?; let response = { let query = client.query(zone.clone(), DNSClass::IN, RecordType::AXFR); query.await.map_err(make_500)? }; if response.response_code() != ResponseCode::NoError { println!("Querrying AXFR of zone {} failed with code {}", *zone, response.response_code()); return ErrorResponse::new( Status::NotFound, "Zone could not be found".into() ).with_details(json!({ "zone_name": zone.to_utf8() })).err(); } let answers = response.answers(); let mut records: Vec<_> = answers.to_vec().into_iter() .map(dns::Record::from) .filter(|record| !matches!(record.rdata, dns::RData::NULL { .. } | dns::RData::DNSSEC(_))) .collect(); // AXFR response ends with SOA, we remove it so it is not doubled in the response. records.pop(); Ok(Json(records)) } #[post("/zones//records", data = "")] pub async fn create_zone_records( mut client: dns::DnsClient, conn: DbConn, user_info: Result, zone: dns::AbsoluteName, new_records: Json> ) -> Result, ErrorResponse> { let user_info = user_info?; let zone_name = zone.to_utf8(); conn.run(move |c| { if user_info.is_admin() { Zone::get_by_name(c, &zone_name) } else { user_info.get_zone(c, &zone_name) } }).await?; // TODO: What about relative names (also in cnames and stuff) let mut bad_records = Vec::new(); let mut records: Vec = Vec::new(); for record in new_records.into_inner().into_iter() { let this_record = record.clone(); if let Ok(record) = record.try_into() { records.push(record); } else { bad_records.push(this_record.clone()); } } let bad_zone_records: Vec<_> = records.iter().filter(|record| !zone.zone_of(record.name())).collect(); // TODO: Get zone class from somewhere instead of always assuming IN let bad_class_records: Vec<_> = records.iter().filter(|record| record.dns_class() != DNSClass::IN).collect(); if !bad_records.is_empty() { return ErrorResponse::new( Status::BadRequest, "Record list contains records that could not been parsed into DNS records".into() ).with_details( json!({ "zone_name": zone.to_utf8(), "records": bad_records }) ).err(); } if !bad_zone_records.is_empty() { return ErrorResponse::new( Status::BadRequest, "Record list contains records whose name does not belong to the zone".into() ).with_details( json!({ "zone_name": zone.to_utf8(), "records": bad_zone_records.into_iter().map(|r| r.clone().into()).collect::>() }) ).err(); } if !bad_class_records.is_empty() { return ErrorResponse::new( Status::BadRequest, "Record list contains records whose class differs from the zone class `IN`".into() ).with_details( json!({ "zone_name": zone.to_utf8(), "records": bad_class_records.into_iter().map(|r| r.clone().into()).collect::>() }) ).err(); } let mut zone_query = Query::new(); zone_query.set_name(zone.clone()) .set_query_class(DNSClass::IN) .set_query_type(RecordType::SOA); let mut message = Message::new(); // TODO: set random / time based id message .set_id(0) .set_message_type(MessageType::Query) .set_op_code(OpCode::Update) .set_recursion_desired(false); message.add_zone(zone_query); message.add_updates(records); { let edns = message.edns_mut(); edns.set_max_payload(1232); edns.set_version(0); } let response = { let query = dns::ClientResponse(client.send(message)); query.await.map_err(make_500)? }; // TODO: better error handling if response.response_code() != ResponseCode::NoError { println!("Update of zone {} failed with code {}", *zone, response.response_code()); return ErrorResponse::new( Status::NotFound, "Update of zone failed".into() ).with_details(json!({ "zone_name": zone.to_utf8() })).err(); } Ok(Json(())) } #[get("/zones")] pub async fn get_zones( conn: DbConn, user_info: Result, ) -> Result>, ErrorResponse> { let user_info = user_info?; let zones = conn.run(move |c| { if user_info.is_admin() { Zone::get_all(c) } else { user_info.get_zones(c) } }).await?; Ok(Json(zones)) } #[post("/zones", data = "")] pub async fn create_zone( conn: DbConn, mut client: dns::DnsClient, user_info: Result, zone_request: Json, ) -> Result, ErrorResponse> { user_info?.check_admin()?; // Check if the zone exists in the DNS server let response = { let query = client.query(zone_request.name.clone(), DNSClass::IN, RecordType::SOA); query.await.map_err(make_500)? }; if response.response_code() != ResponseCode::NoError { println!("Querrying SOA of zone {} failed with code {}", *zone_request.name, response.response_code()); return ErrorResponse::new( Status::NotFound, format!("Zone {} could not be found", *zone_request.name) ).err() } let zone = conn.run(move |c| { Zone::create_zone(c, zone_request.into_inner()) }).await?; Ok(Json(zone)) } #[post("/zones//members", data = "")] pub async fn add_member_to_zone<'r>( conn: DbConn, zone: dns::AbsoluteName, user_info: Result, zone_member_request: Json ) -> Result, ErrorResponse> { let user_info = user_info?; let zone_name = zone.to_utf8(); conn.run(move |c| { let zone = if user_info.is_admin() { Zone::get_by_name(c, &zone_name) } else { user_info.get_zone(c, &zone_name) }?; let new_member = LocalUser::get_user_by_uuid(c, &zone_member_request.id)?; zone.add_member(&c, &new_member) }).await?; Response::build() .status(Status::Created) // TODO: change this? .ok() }