diff --git a/src/requests/delegation.cr b/src/requests/delegation.cr index 5f970be..2df1088 100644 --- a/src/requests/delegation.cr +++ b/src/requests/delegation.cr @@ -18,4 +18,44 @@ class DNSManager::Request end end DNSManager.requests << DelegateDomain + + IPC::JSON.message EditDelegation, 26 do + property domain : String + property nameserver1 : String + property nameserver2 : String + def initialize(@domain, @nameserver1, @nameserver2) + end + + def to_s(io : IO) + super io + io << " (domain: #{@domain}, ns1: #{@nameserver1}, ns2: #{@nameserver2}" + end + + def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON + user = dnsmanagerd.get_logged_user event + return Response::ErrorUserNotLogged.new unless user + dnsmanagerd.storage.edit_delegation user.uid, @domain, @nameserver1, @nameserver2 + end + end + DNSManager.requests << EditDelegation + + IPC::JSON.message ResetDelegation, 27 do + property domain : String + property nameserver1 : String + property nameserver2 : String + def initialize(@domain, @nameserver1, @nameserver2) + end + + def to_s(io : IO) + super io + io << " (domain: #{@domain}, ns1: #{@nameserver1}, ns2: #{@nameserver2}" + end + + def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON + user = dnsmanagerd.get_logged_user event + return Response::ErrorUserNotLogged.new unless user + dnsmanagerd.storage.reset_delegation user.uid, @domain + end + end + DNSManager.requests << ResetDelegation end diff --git a/src/storage.cr b/src/storage.cr index 809fe9c..b0f4293 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -24,7 +24,7 @@ class DNSManager::Storage getter root : String getter zonefiledir : String - getter delegationdir : String + getter delegationdir : String property dnsmanagerd : DNSManager::Service? = nil @@ -165,34 +165,24 @@ class DNSManager::Storage Response::GeneratedZone.new domain, (String.new io.buffer, io.pos) end - # Adds a new domain. - def new_domain(user_id : UserDataID, domain : String) : IPC::JSON + def remove_first_label (domain : String) : String + domain.gsub /^[^.]+./, "" + end + + # `has_valid_tld?` takes a domain and returns its TLD if it matches one of the available TLD. + def has_valid_tld? (domain : String) : String? accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil! + + new_domain_tld = remove_first_label domain + + tld = accepted_domains & [ new_domain_tld ] + + return tld.pop unless tld.empty? + end + + # Provides a zone based on a template (selected based on the tld). + def get_new_zone(tld : String, domain : String) : Zone template_directory = dnsmanagerd.configuration.template_directory - - # Prevent future very confusing errors. - domain = domain.downcase - - return Response::DomainAlreadyExists.new if zones_by_domain.get? domain - - # Verify if the domain is acceptable. - matching_domains = accepted_domains.select { |adomain| domain.ends_with? adomain } - unless matching_domains.size > 0 - Baguette::Log.warning "trying to add an unacceptable domain: '#{domain}'" - return Response::UnacceptableDomain.new - end - - matching_domains.each do |md| - # Prevent empty domains (from crafted requests) to be accepted. - return Response::InvalidDomainName.new unless (domain.chomp md).size > 1 - #Baguette::Log.info "Add new domain #{domain} (matching domain #{md})" - end - - # Verify the domain name validity. - return Response::InvalidDomainName.new unless Zone.is_domain_valid? domain - - # Fill a template zone. - tld = matching_domains.pop default_zone = Zone.from_json File.read "#{template_directory}/#{tld}.json" # Replace domain by the real domain. @@ -204,17 +194,43 @@ class DNSManager::Storage # configuration. default_zone.reset_serial + default_zone + end + + # Adds a new domain. + def new_domain(user_id : UserDataID, domain : String) : IPC::JSON + + # Prevent future very confusing errors. + domain = domain.downcase + + # First test: verify the domain name validity. + return Response::InvalidDomainName.new unless Zone.is_domain_valid? domain + + # Second test: check on the TLD. + # This also rejects empty domains and a few other invalid names as a side effect. + new_domain_tld = has_valid_tld? domain + if new_domain_tld.nil? + Baguette::Log.warning "trying to add an unacceptable domain: '#{domain}'" + return Response::UnacceptableDomain.new + end + + # Last check: verify the availability of the domain name. + return Response::DomainAlreadyExists.new if zones_by_domain.get? domain + # # Actually write data on-disk. # + Baguette::Log.debug "Add new domain #{domain} (matching domain #{new_domain_tld})" + # Add the new domain. the_new_domain = Domain.new domain the_new_domain.owners = [user_id] @domains << the_new_domain - # Add the new zone in the database. - update_zone default_zone + # Fill a template zone then add it to the database. + the_new_zone = get_new_zone new_domain_tld, domain + update_zone the_new_zone Response::DomainAdded.new domain end @@ -340,12 +356,13 @@ class DNSManager::Storage # Any modification of the zone must be performed here. # This function updates the SOA serial before storing the modified zone. - # Also, this function generate the Bind9 file. + # Also, this function generates the Bind9 file unless the zone is delegated. def update_zone(zone : Zone) : Nil zone.update_serial zones_by_domain.update_or_create zone.domain, zone - generate_bind9_zonefile zone.domain + # Do not generate a bind9 file if the zone is delegated. + generate_bind9_zonefile zone.domain unless zone.delegation end def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON @@ -449,6 +466,67 @@ class DNSManager::Storage Response::Zone.new zone end + # `edit_delegation` enables users to update the delegation name servers. + def edit_delegation(user_id : UserDataID, domain_name : String, + nameserver1 : String, nameserver2 : String) : IPC::JSON + + # Verifies the provided name server domains. + return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver1 + return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver2 + + zone = zone_must_exist! domain_name + user_should_own! user_id, domain_name + + # Make sure both name servers are "absolute" domain names. + ns1 = if nameserver1.ends_with? '.' + nameserver1 + else + "#{nameserver1}." + end + ns2 = if nameserver2.ends_with? '.' + nameserver2 + else + "#{nameserver2}." + end + + # Clone the zone because the update mechanism needs the old values in order to remove their files. + zone_clone = zone.clone + + # Edit the name servers used for delegation. + zone_clone.delegation = Zone::Delegation.new ns1, ns2 + + # On-disk update of the name servers used for delegation. + zone_clone.update_delegation @delegationdir + + # Once the new delegation file has been written, the script generating the (root) zone file must + # be informed by touching a file (named "delegation token file" in the source code). + update_delegation_token_file + + update_zone zone_clone + + Response::Success.new + end + + # `reset_delegation` removes the delegation of a domain, auto-fill the zone with a template. + def reset_delegation(user_id : UserDataID, domain_name : String) : IPC::JSON + user_should_own! user_id, domain_name + + # Removes all traces of the zone (including the delegation file). + wipe_zone user_id, domain_name + + domain_tld = has_valid_tld? domain_name + if domain_tld.nil? + Baguette::Log.warning "trying to add an unacceptable domain: '#{domain_name}'" + return Response::UnacceptableDomain.new + end + + # Creates and auto-fill a new zone from a template then add it to the database. + the_new_zone = get_new_zone domain_tld, domain_name + update_zone the_new_zone + + Response::Success.new + end + # Deletes the zone and its tokens to wipe the zone data then delegates the domain. def delegate_domain(user_id : UserDataID, domain_name : String, nameserver1 : String, nameserver2 : String) : IPC::JSON diff --git a/src/storage/zone.cr b/src/storage/zone.cr index bc7a927..0d6facf 100644 --- a/src/storage/zone.cr +++ b/src/storage/zone.cr @@ -1029,6 +1029,7 @@ class DNSManager::Storage::Zone false end + # TODO: this verification is superficial. To be replaced. # Valid names: valid subdomains, wildcards alone or followed by a subdomain. def self.is_valid_name?(subdomain) : Bool if subdomain =~ subdomain_pattern