Compare commits
33 Commits
Author | SHA1 | Date |
---|---|---|
|
38da24fe66 | |
|
b633dbe740 | |
|
0eca58ffa8 | |
|
98b5ed2638 | |
|
2d64da170e | |
|
1e66262884 | |
|
519533c02d | |
|
a51fd8b31b | |
|
8d9f4051e6 | |
|
cc4df1775c | |
|
835415caac | |
|
516ccf1e58 | |
|
749889aad8 | |
|
8119bf7f57 | |
|
2509e8af15 | |
|
cb8b0c93a1 | |
|
925a456de7 | |
|
b53b31b584 | |
|
5cbcbdaa8f | |
|
de8beb0df8 | |
|
7bee53d625 | |
|
234259a9d0 | |
|
4923fb34f9 | |
|
158d772727 | |
|
723c1a83a0 | |
|
ebd9bd75a5 | |
|
4606f852cc | |
|
1f69839333 | |
|
a188d28f1d | |
|
8cafca13be | |
|
a888551bbc | |
|
40fcc9c66e | |
|
61bace491c |
|
@ -3,3 +3,4 @@ bin
|
|||
drop
|
||||
lib
|
||||
templates/
|
||||
docs/
|
||||
|
|
123
Makefile
123
Makefile
|
@ -8,101 +8,108 @@ else
|
|||
LOGIN_OPT = -l $(LOGIN)
|
||||
endif
|
||||
|
||||
# No idea why, but I need that to run applications. Ignore that.
|
||||
#LD_P ?= LD_PRELOAD=/usr/local/lib/libipc.so.0
|
||||
|
||||
OPTS ?= --progress
|
||||
|
||||
Q ?= @
|
||||
|
||||
SHOULD_UPDATE = ./bin/should-update
|
||||
SOURCE_FILES = $(wildcard src/*.cr src/*/*.cr src/*/*/*.cr)
|
||||
|
||||
####################
|
||||
### REQUEST EXAMPLES
|
||||
####################
|
||||
|
||||
DOMAIN ?= example.com
|
||||
build-write-zone-file: tools/write-zone-file.cr
|
||||
$(Q)-([ ! -f bin/write-zone-file ] || [ tools/write-zone-file.cr -nt bin/write-zone-file ]) && shards build write-zone-file $(OPTS)
|
||||
bin/write-zone-file: tools/write-zone-file.cr
|
||||
$(Q)-shards build write-zone-file $(OPTS)
|
||||
build-write-zone-file: bin/write-zone-file
|
||||
zone-file: build-write-zone-file
|
||||
$(Q)./bin/write-zone-file $(DOMAIN)
|
||||
|
||||
build-write-template-zone-file: tools/write-template-zone-file.cr
|
||||
$(Q)-([ ! -f bin/write-template-zone-file ] || [ tools/write-template-zone-file.cr -nt bin/write-template-zone-file ]) && shards build write-template-zone-file $(OPTS)
|
||||
bin/write-template-zone-file: tools/write-template-zone-file.cr
|
||||
$(Q)-shards build write-template-zone-file $(OPTS)
|
||||
build-write-template-zone-file: bin/write-template-zone-file
|
||||
zone-basic-template-file: build-write-template-zone-file
|
||||
$(Q)./bin/write-template-zone-file $(DOMAIN)
|
||||
|
||||
bin/powerdns-sync: tools/powerdns-sync.cr
|
||||
$(Q)-shards build powerdns-sync $(OPTS)
|
||||
build-powerdns-sync: bin/powerdns-sync
|
||||
|
||||
VERBOSITY ?= 4
|
||||
run-client-verbosity:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client admin maintenance verbosity $(VERBOSITY) $(LOGIN_OPT)
|
||||
|
||||
run-client-domain-add:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user domain add $(DOMAIN) $(LOGIN_OPT)
|
||||
|
||||
run-client-domain-del:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user domain del $(DOMAIN) $(LOGIN_OPT)
|
||||
|
||||
run-client-domain-list:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user domain list $(LOGIN_OPT)
|
||||
|
||||
run-client-zone-add:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user zone add $(DOMAIN).json $(LOGIN_OPT)
|
||||
|
||||
run-client-zone-get:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user zone get $(DOMAIN) $(LOGIN_OPT)
|
||||
run-client-verbosity:; $(Q)./bin/dnsmanager-client admin maintenance verbosity $(VERBOSITY) $(LOGIN_OPT)
|
||||
run-client-domain-add:; $(Q)./bin/dnsmanager-client user domain add $(DOMAIN) $(LOGIN_OPT)
|
||||
run-client-domain-del:; $(Q)./bin/dnsmanager-client user domain del $(DOMAIN) $(LOGIN_OPT)
|
||||
run-client-domain-list:; $(Q)./bin/dnsmanager-client user domain list $(LOGIN_OPT)
|
||||
run-client-zone-add:; $(Q)./bin/dnsmanager-client user zone add $(DOMAIN).json $(LOGIN_OPT)
|
||||
run-client-zone-get:; $(Q)./bin/dnsmanager-client user zone get $(DOMAIN) $(LOGIN_OPT)
|
||||
|
||||
RRID ?= 1
|
||||
NAME ?=
|
||||
TTL ?= 3600
|
||||
TARGET ?=
|
||||
run-client-rr-add-a:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user rr add A $(DOMAIN) $(NAME) $(TTL) $(TARGET) $(LOGIN_OPT)
|
||||
|
||||
run-client-rr-update-a:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user rr update A $(DOMAIN) $(RRID) $(NAME) $(TTL) $(TARGET) $(LOGIN_OPT)
|
||||
|
||||
run-client-rr-del:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user rr del $(DOMAIN) $(RRID) $(LOGIN_OPT)
|
||||
run-client-rr-add-a:; $(Q)./bin/dnsmanager-client user rr add A $(DOMAIN) $(NAME) $(TTL) $(TARGET) $(LOGIN_OPT)
|
||||
run-client-rr-update-a:; $(Q)./bin/dnsmanager-client user rr update A $(DOMAIN) $(RRID) $(NAME) $(TTL) $(TARGET) $(LOGIN_OPT)
|
||||
run-client-rr-del:; $(Q)./bin/dnsmanager-client user rr del $(DOMAIN) $(RRID) $(LOGIN_OPT)
|
||||
run-client-genzones:; $(Q)./bin/dnsmanager-client admin genall $(LOGIN_OPT)
|
||||
|
||||
##################
|
||||
### SETUP COMMANDS
|
||||
##################
|
||||
|
||||
DBDIR=/tmp/DATA-dnsmanagerd
|
||||
run-dnsmanagerd:
|
||||
$(Q)$(LD_P) ./bin/dnsmanagerd -v $(VERBOSITY) -r $(DBDIR)
|
||||
bin/dnsmanagerd: $(SOURCE_FILES); $(Q)shards build dnsmanagerd $(OPTS)
|
||||
build-server: bin/dnsmanagerd
|
||||
run-dnsmanagerd:; $(Q)./bin/dnsmanagerd -v $(VERBOSITY) -r $(DBDIR)
|
||||
|
||||
bin/dnsmanager-client: $(SOURCE_FILES); $(Q)shards build dnsmanager-client $(OPTS)
|
||||
build-client: bin/dnsmanager-client
|
||||
|
||||
PORT ?= 8082
|
||||
ADDR ?=
|
||||
run-token-handler:
|
||||
$(Q)$(LD_P) ./bin/token-handler $(PORT) $(ADDR)
|
||||
bin/token-handler: tools/token-handler.cr; $(Q)shards build token-handler $(OPTS)
|
||||
build-token-handler: bin/token-handler
|
||||
run-token-handler: bin/token-handler; $(Q)./bin/token-handler $(PORT) $(ADDR)
|
||||
|
||||
build-server:
|
||||
$(Q)-$(SHOULD_UPDATE) bin/dnsmanagerd && shards build dnsmanagerd $(OPTS)
|
||||
build: build-server build-client build-token-handler build-powerdns-sync
|
||||
|
||||
build-client:
|
||||
$(Q)-$(SHOULD_UPDATE) bin/dnsmanager-client && shards build dnsmanager-client $(OPTS)
|
||||
print-messages:; cat src/requests/*.cr | ./bin/get-messages.awk
|
||||
print-message-numbers:; make -s print-messages | grep -E "^[0-9]" | sort -n
|
||||
print-messages-no-comments:; make -s print-messages | grep -vE '^[[:blank:]]+#'
|
||||
print-response-messages:; cat src/responses/*.cr | ./bin/get-messages.awk
|
||||
print-response-message-numbers:; make -s print-response-messages | grep -E "^[0-9]" | sort -n
|
||||
print-response-messages-no-comments:; make -s print-response-messages | grep -vE '^[[:blank:]]+#'
|
||||
|
||||
build-token-handler:
|
||||
$(Q)shards build token-handler $(OPTS)
|
||||
# format: nb-domains <TAB> login <TAB> domain1 <TAB> domain2 <TAB> domain3
|
||||
MIGRATION_FILE_TARGET = /tmp/dnsmanagerd-migration
|
||||
SQLDB = /tmp/usrdb
|
||||
POWERDNS_ZONEDIR = /var/powerdns
|
||||
BINDDIR = /tmp/DATA-dnsmanagerd/bind9-zones
|
||||
$(MIGRATION_FILE_TARGET):; ./bin/sql-to-migration-format.awk < $(SQLDB) > $(MIGRATION_FILE_TARGET)
|
||||
run-migration-client:; ./bin/dnsmanager-client admin migration-script $(MIGRATION_FILE_TARGET) $(LOGIN)
|
||||
migration-file: $(MIGRATION_FILE_TARGET)
|
||||
copy-old-zones: ; cd $(BINDDIR) && for i in * ; do cp -v /tmp/rndczones/$$i . ; done
|
||||
/tmp/rndczones:
|
||||
@echo "you forgot to get a copy of old bind zones here: /tmp/rndczones"
|
||||
exit 1
|
||||
powerdns-create-zonedir:
|
||||
-mkdir -p $(POWERDNS_ZONEDIR)
|
||||
cp -v $(BINDDIR)/* $(POWERDNS_ZONEDIR)
|
||||
powerdns-add-zones: powerdns-create-zonedir
|
||||
cd $(POWERDNS_ZONEDIR) && for i in *; do pdns_control bind-add-zone $$i $(POWERDNS_ZONEDIR)/$$i; done
|
||||
migration: build-client migration-file run-migration-client run-client-genzones copy-old-zones
|
||||
@echo "next as root: make powerdns-add-zones"
|
||||
|
||||
build: build-server build-client build-token-handler
|
||||
doc:
|
||||
crystal docs src/main.cr src/client.cr lib/authd/src/client.cr
|
||||
|
||||
print-messages:
|
||||
cat src/requests/*.cr | ./bin/get-messages.awk
|
||||
print-message-numbers:
|
||||
make -s print-messages | grep -E "^[0-9]" | sort -n
|
||||
print-messages-without-comments:
|
||||
make -s print-messages | grep -vE '^[[:blank:]]+#'
|
||||
print-response-messages:
|
||||
cat src/responses/*.cr | ./bin/get-messages.awk
|
||||
print-response-message-numbers:
|
||||
make -s print-response-messages | grep -E "^[0-9]" | sort -n
|
||||
print-response-messages-without-comments:
|
||||
make -s print-response-messages | grep -vE '^[[:blank:]]+#'
|
||||
HTTPD_ACCESS_LOGS ?= /tmp/access-dnsmanager-docs.log
|
||||
HTTPD_ADDR ?= 127.0.0.1
|
||||
HTTPD_PORT ?= 9001
|
||||
DIR ?= docs
|
||||
serve-doc:
|
||||
darkhttpd $(DIR) --addr $(HTTPD_ADDR) --port $(HTTPD_PORT) --log $(HTTPD_ACCESS_LOGS)
|
||||
|
||||
wipe-db:
|
||||
rm -r $(DBDIR)
|
||||
|
||||
release:
|
||||
make build-server OPTS="--release --progress"
|
||||
make build-server OPTS="--release --progress --no-debug"
|
||||
|
|
4
TODO.md
4
TODO.md
|
@ -9,6 +9,10 @@ TODO:
|
|||
* (server) Resource Records to add, del and modify
|
||||
* (server) Zone validity on modification
|
||||
* (client) check for errors in the list of possible returned messages
|
||||
- optimization: RAMOnly-DB for connected users
|
||||
1. dnsmanagerd should be able to (un)subscribe for a user data
|
||||
2. avoid requests to AuthD (don't ask for user data twice)
|
||||
3. AuthD should send updates on a user in real-time
|
||||
|
||||
DONE:
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/awk -f
|
||||
|
||||
BEGIN {
|
||||
data = ""
|
||||
}
|
||||
|
||||
/^\]/ {
|
||||
gsub("},$","}",data)
|
||||
print data
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
/},$/ {
|
||||
print data
|
||||
data = $0
|
||||
next
|
||||
}
|
||||
|
||||
{
|
||||
data = $0
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/awk -f
|
||||
|
||||
BEGIN {
|
||||
data = ""
|
||||
}
|
||||
|
||||
/^\[/ || /^\]/ {
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
/^ {/ && /},$/ {
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
/^ {/ {
|
||||
data = $0
|
||||
next
|
||||
}
|
||||
|
||||
/},$/ {
|
||||
data = sprintf("%s %s", data, $0)
|
||||
print data
|
||||
next
|
||||
}
|
||||
|
||||
{
|
||||
data = sprintf("%s %s", data, $0)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/gawk -f
|
||||
|
||||
BEGIN {
|
||||
OFS = "\t"
|
||||
}
|
||||
|
||||
/^\[/ || /^\]/ {
|
||||
next
|
||||
}
|
||||
|
||||
{
|
||||
domain = $0
|
||||
login = $0
|
||||
gsub("^.+domain\": \"", "", domain)
|
||||
gsub("\",\"login\": .+", "", domain)
|
||||
gsub(".+\",\"login\": \"", "", login)
|
||||
gsub("\" }.?$", "", login)
|
||||
data[login][domain] = 1
|
||||
}
|
||||
|
||||
END {
|
||||
for ( login in data ) {
|
||||
domains = ""
|
||||
nb = 0
|
||||
for ( domain in data[login] ) {
|
||||
nb += 1
|
||||
domains = (domains "\t" domain)
|
||||
}
|
||||
print nb, login, domains
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Should we run the build?
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
exec >& 2
|
||||
echo "Usage: $0 <exe>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exe=$1
|
||||
|
||||
# If the binary hasn't already be compiled.
|
||||
[ -f "${exe}" ] || exit 0
|
||||
|
||||
v=`find src/ -type f -newer "${exe}" | wc -l`
|
||||
test "${v}" != "0"
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/awk -f
|
||||
|
||||
BEGIN {
|
||||
OFS = "\t"
|
||||
}
|
||||
|
||||
{
|
||||
login = $1
|
||||
domain = $3
|
||||
data[login][domain] = 1
|
||||
}
|
||||
|
||||
END {
|
||||
for ( login in data ) {
|
||||
domains = ""
|
||||
nb = 0
|
||||
for ( domain in data[login] ) {
|
||||
nb += 1
|
||||
domains = (domains "\t" domain)
|
||||
}
|
||||
print nb, login, domains
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ dependencies:
|
|||
ipc:
|
||||
git: https://git.baguette.netlib.re/Baguette/ipc.cr
|
||||
dodb:
|
||||
branch: master
|
||||
git: https://git.baguette.netlib.re/Baguette/dodb.cr
|
||||
authd:
|
||||
git: https://git.baguette.netlib.re/Baguette/authd
|
||||
|
@ -32,5 +33,7 @@ targets:
|
|||
main: tools/write-template-zone-file.cr
|
||||
token-handler:
|
||||
main: tools/token-handler.cr
|
||||
powerdns-sync:
|
||||
main: tools/powerdns-sync.cr
|
||||
|
||||
license: ISC
|
||||
|
|
|
@ -51,7 +51,23 @@ class DNSManager::Client < IPC
|
|||
def user_domain_add(domain : String)
|
||||
request = Request::NewDomain.new domain
|
||||
send_now request
|
||||
parse_message [ Response::Success ], read
|
||||
parse_message [ Response::DomainAdded,
|
||||
Response::DomainAlreadyExists,
|
||||
Response::InvalidDomainName,
|
||||
Response::UnacceptableDomain
|
||||
], read
|
||||
end
|
||||
|
||||
# Migration: add a domain for a user.
|
||||
def migration(login : String, domain : String)
|
||||
request = Request::Migration.new login, domain
|
||||
send_now request
|
||||
parse_message [ Response::DomainAdded,
|
||||
Response::DomainAlreadyExists,
|
||||
Response::InvalidDomainName,
|
||||
Response::UnacceptableDomain,
|
||||
Response::UnknownUser
|
||||
], read
|
||||
end
|
||||
|
||||
# Remove a domain.
|
||||
|
|
|
@ -29,6 +29,7 @@ class Actions
|
|||
# Maintenance.
|
||||
@the_call["admin-maintenance"] = ->admin_maintenance
|
||||
@the_call["admin-generate-zonefile"] = ->admin_generate_zonefile
|
||||
@the_call["admin-migration-script"] = ->admin_migration_script
|
||||
@the_call["admin-generate-all-zonefiles"] = ->admin_generate_all_zonefiles
|
||||
|
||||
# Domain operations.
|
||||
|
@ -86,6 +87,60 @@ class Actions
|
|||
end
|
||||
end
|
||||
|
||||
# Migration script.
|
||||
#
|
||||
# Usage: dnsmanager-client admin migration-script user-db.txt
|
||||
#
|
||||
# user-db.txt should be formated as: login <TAB> domain1 <TAB> domain2 <TAB> domain3 (etc.)
|
||||
def admin_migration_script
|
||||
filename = Context.args.not_nil!.[0]
|
||||
File.each_line(filename) do |line|
|
||||
data = line.split "\t"
|
||||
_ = data.shift
|
||||
login = data.shift
|
||||
data.select! { |x| x != "" }
|
||||
nb_domains = data.size
|
||||
begin
|
||||
i = 0
|
||||
data.each do |domain|
|
||||
STDOUT.write "migrating '#{i}/#{nb_domains}' domains for '#{login}': #{ "%.100s" % domain }".to_slice
|
||||
STDOUT.write ((" " * 80) + "\r").to_slice
|
||||
i += 1
|
||||
if (login == "Régis")
|
||||
req = DNSManager::Request::Migration.new login, domain
|
||||
puts ""
|
||||
Baguette::Log.info "#{req.to_json.to_s}"
|
||||
end
|
||||
response = @dnsmanagerd.migration login, domain
|
||||
case response
|
||||
when DNSManager::Response::DomainAdded
|
||||
# Do nothing.
|
||||
else
|
||||
case response
|
||||
when DNSManager::Response::DomainAlreadyExists
|
||||
#Baguette::Log.error "error: domain name '#{domain}' already exists"
|
||||
when DNSManager::Response::InvalidDomainName
|
||||
puts ""
|
||||
Baguette::Log.error "error: invalid domain name '#{domain}'"
|
||||
when DNSManager::Response::UnacceptableDomain
|
||||
puts ""
|
||||
Baguette::Log.error "error: unacceptable domain name '#{domain}' (login: '#{login}')"
|
||||
when DNSManager::Response::UnknownUser
|
||||
puts ""
|
||||
Baguette::Log.error "error: unknown user #{login}"
|
||||
break
|
||||
else
|
||||
puts ""
|
||||
Baguette::Log.error "error: unknown error"
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue e
|
||||
puts "error for generate_zonefile: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def admin_generate_all_zonefiles
|
||||
pp! @dnsmanagerd.generate_all_zonefiles
|
||||
rescue e
|
||||
|
|
|
@ -59,11 +59,13 @@ def parsing_cli(authd_config : Baguette::Configuration::Auth)
|
|||
# either we test with the exact expected number of arguments or the least.
|
||||
if exact = nexact
|
||||
if args.size != exact
|
||||
Baguette::Log.error "Wrong number of parameters: expected #{exact}, got #{args.size}"
|
||||
Baguette::Log.error "#{parser}"
|
||||
exit 1
|
||||
end
|
||||
elsif least = at_least
|
||||
if args.size < least
|
||||
Baguette::Log.error "Wrong number of parameters: expected at least #{least}, got #{args.size}"
|
||||
Baguette::Log.error "#{parser}"
|
||||
exit 1
|
||||
end
|
||||
|
@ -114,6 +116,13 @@ def parsing_cli(authd_config : Baguette::Configuration::Auth)
|
|||
unrecognized_args_to_context_args.call parser, 0, nil
|
||||
end
|
||||
|
||||
# Migration script.
|
||||
parser.on("migration-script", "Migrate domains from dnsmanager v1.") do
|
||||
Baguette::Log.info "migration script, provide domains to users."
|
||||
Context.command = "admin-migration-script"
|
||||
parser.banner = "COMMAND: admin migration-script user-db.txt"
|
||||
unrecognized_args_to_context_args.call parser, 1, nil
|
||||
end
|
||||
end
|
||||
|
||||
# User section.
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
module DNSManager
|
||||
class Exception < ::Exception
|
||||
end
|
||||
|
||||
class NotLoggedException < ::Exception
|
||||
end
|
||||
|
||||
class NoOwnershipException < ::Exception
|
||||
end
|
||||
|
||||
class AuthorizationException < ::Exception
|
||||
end
|
||||
|
||||
class UnknownUserException < ::Exception
|
||||
end
|
||||
|
||||
class RRReadOnlyException < ::Exception
|
||||
property domain : String
|
||||
property rr : DNSManager::Storage::Zone::ResourceRecord
|
||||
def initialize(@domain, @rr)
|
||||
end
|
||||
end
|
||||
|
||||
class CannotCheckPermissionsException < ::Exception
|
||||
property uid : UserDataID
|
||||
property resource : String
|
||||
def initialize(@uid, @resource)
|
||||
end
|
||||
end
|
||||
|
||||
class DomainNotFoundException < ::Exception
|
||||
end
|
||||
|
||||
class RRNotFoundException < ::Exception
|
||||
end
|
||||
|
||||
class TokenNotFoundException < ::Exception
|
||||
end
|
||||
end
|
350
src/main.cr
350
src/main.cr
|
@ -3,303 +3,99 @@ require "option_parser"
|
|||
|
||||
require "ipc"
|
||||
require "ipc/json"
|
||||
|
||||
require "authd"
|
||||
require "baguette-crystal-base"
|
||||
|
||||
require "./config"
|
||||
|
||||
module DNSManager
|
||||
class Exception < ::Exception
|
||||
end
|
||||
class DomainNotFoundException < ::Exception
|
||||
end
|
||||
class UnknownUserException < ::Exception
|
||||
end
|
||||
class RRReadOnlyException < ::Exception
|
||||
property domain : String
|
||||
property rr : DNSManager::Storage::Zone::ResourceRecord
|
||||
def initialize(@domain, @rr)
|
||||
end
|
||||
end
|
||||
class CannotCheckPermissionsException < ::Exception
|
||||
property uid : UserDataID
|
||||
property resource : String
|
||||
def initialize(@uid, @resource)
|
||||
end
|
||||
end
|
||||
class AuthorizationException < ::Exception
|
||||
end
|
||||
class NoOwnershipException < ::Exception
|
||||
end
|
||||
class NotLoggedException < ::Exception
|
||||
end
|
||||
class RRNotFoundException < ::Exception
|
||||
end
|
||||
class TokenNotFoundException < ::Exception
|
||||
end
|
||||
end
|
||||
|
||||
require "./service"
|
||||
|
||||
require "./storage.cr"
|
||||
require "./network.cr"
|
||||
|
||||
# First option parsing, same with all Baguette (service) applications.
|
||||
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
|
||||
|
||||
class DNSManager::Service < IPC
|
||||
property configuration : Baguette::Configuration::DNSManager
|
||||
getter storage : DNSManager::Storage
|
||||
getter logged_users : Hash(Int32, AuthD::User::Public)
|
||||
|
||||
property authd : AuthD::Client
|
||||
|
||||
def initialize(@configuration)
|
||||
super()
|
||||
@storage = DNSManager::Storage.new @configuration.storage_directory, @configuration.recreate_indexes
|
||||
|
||||
@logged_users = Hash(Int32, AuthD::User::Public).new
|
||||
|
||||
# TODO: auth service isn't in the FDs pool.
|
||||
# If the service crashes, dnsmanagerd won't know it.
|
||||
@authd = AuthD::Client.new
|
||||
response = authd.login? @configuration.login, @configuration.pass.not_nil!
|
||||
case response
|
||||
when AuthD::Response::Login
|
||||
uid = response.uid
|
||||
token = response.token
|
||||
Baguette::Log.info "Authenticated as #{@configuration.login} #{uid}, token: #{token}"
|
||||
else
|
||||
@authd.close
|
||||
raise "Cannot authenticate to authd with login #{@configuration.login}: #{response}."
|
||||
end
|
||||
|
||||
self.timer @configuration.ipc_timer
|
||||
self.service_init @configuration.service_name
|
||||
end
|
||||
|
||||
def get_logged_user(event : IPC::Event)
|
||||
@logged_users[event.fd]?
|
||||
end
|
||||
|
||||
def decode_token(token : String)
|
||||
@authd.decode_token token
|
||||
end
|
||||
|
||||
def check_permissions(uid : UInt32, resource : String) : AuthD::User::PermissionLevel
|
||||
response = @authd.check_permission uid, "dnsmanager", resource
|
||||
case response
|
||||
when AuthD::Response::PermissionCheck
|
||||
return response.permission
|
||||
end
|
||||
raise CannotCheckPermissionsException.new uid, resource
|
||||
rescue e
|
||||
Baguette::Log.error "error while checking permissions: #{e}"
|
||||
raise CannotCheckPermissionsException.new uid, resource
|
||||
end
|
||||
|
||||
def assert_permissions!(uid : UInt32, resource : String, perms : AuthD::User::PermissionLevel)
|
||||
if check_permissions(uid, resource) < perms
|
||||
raise AuthorizationException.new
|
||||
end
|
||||
end
|
||||
|
||||
def handle_request(event : IPC::Event)
|
||||
request_start = Time.utc
|
||||
|
||||
array = event.message.not_nil!
|
||||
slice = Slice.new array.to_unsafe, array.size
|
||||
message = IPCMessage::TypedMessage.deserialize slice
|
||||
request = DNSManager.requests.parse_ipc_json message.not_nil!
|
||||
|
||||
if request.nil?
|
||||
raise "unknown request type"
|
||||
end
|
||||
|
||||
reqname = request.class.name.sub /^DNSManager::Request::/, ""
|
||||
|
||||
response = begin
|
||||
request.handle self, event
|
||||
rescue e : AuthorizationException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} authorization error"
|
||||
Response::Error.new "authorization error"
|
||||
rescue e : DomainNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} domain not found"
|
||||
Response::DomainNotFound.new
|
||||
rescue e : CannotCheckPermissionsException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'"
|
||||
Response::InsufficientRights.new
|
||||
rescue e : UnknownUserException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} unknown user"
|
||||
Response::UnknownUser.new
|
||||
rescue e : NoOwnershipException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} no ownership error"
|
||||
Response::NoOwnership.new
|
||||
rescue e : NotLoggedException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} user not logged"
|
||||
Response::Error.new "user not logged"
|
||||
rescue e : RRNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR not found"
|
||||
Response::RRNotFound.new
|
||||
rescue e : TokenNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} Token not found"
|
||||
Response::Error.new "token not found"
|
||||
rescue e : RRReadOnlyException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR is read only"
|
||||
Response::RRReadOnly.new e.domain, e.rr
|
||||
rescue e # Generic case
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} generic error #{e}"
|
||||
Response::Error.new "generic error"
|
||||
end
|
||||
|
||||
# If clients sent requests with an “id” field, it is copied
|
||||
# in the responses. Allows identifying responses easily.
|
||||
response.id = request.id
|
||||
|
||||
schedule event.fd, response
|
||||
|
||||
duration = Time.utc - request_start
|
||||
|
||||
response_name = response.class.name.sub /^DNSManager::Response::/, ""
|
||||
|
||||
if response.is_a? DNSManager::Response::Error
|
||||
Baguette::Log.warning "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name} (#{response.reason})"
|
||||
else
|
||||
if reqname != "KeepAlive" || @configuration.print_keepalive
|
||||
Baguette::Log.debug "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
Baguette::Log.title "Starting #{@configuration.service_name}"
|
||||
|
||||
self.loop do |event|
|
||||
begin
|
||||
case event.type
|
||||
when LibIPC::EventType::Timer
|
||||
Baguette::Log.debug "Timer." if @configuration.print_ipc_timer
|
||||
|
||||
when LibIPC::EventType::Connection
|
||||
Baguette::Log.debug "New connection!" if @configuration.print_ipc_connection
|
||||
|
||||
when LibIPC::EventType::Disconnection
|
||||
Baguette::Log.debug "Disconnection from #{event.fd}." if @configuration.print_ipc_disconnection
|
||||
@logged_users.delete event.fd
|
||||
|
||||
when LibIPC::EventType::MessageTx
|
||||
Baguette::Log.debug "Message sent to #{event.fd}." if @configuration.print_ipc_message_sent
|
||||
|
||||
when LibIPC::EventType::MessageRx
|
||||
Baguette::Log.debug "Message received from #{event.fd}." if @configuration.print_ipc_message_received
|
||||
handle_request event
|
||||
|
||||
else
|
||||
Baguette::Log.warning "Unhandled IPC event: #{event.class}."
|
||||
if event.responds_to?(:fd)
|
||||
fd = event.fd
|
||||
Baguette::Log.warning "closing #{fd}"
|
||||
close fd
|
||||
@logged_users.delete fd
|
||||
end
|
||||
end
|
||||
|
||||
rescue exception
|
||||
Baguette::Log.error "exception: #{typeof(exception)} - #{exception.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
# DNSManagerd configuration.
|
||||
configuration = if no_configuration
|
||||
Baguette::Log.info "do not load a configuration file."
|
||||
Baguette::Configuration::DNSManager.new
|
||||
else
|
||||
# In case there is a configuration file helping with the parameters.
|
||||
Baguette::Configuration::DNSManager.get(configuration_file) ||
|
||||
Baguette::Configuration::DNSManager.new
|
||||
end
|
||||
|
||||
|
||||
def main
|
||||
|
||||
# First option parsing, same with all Baguette (service) applications.
|
||||
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
|
||||
|
||||
# DNSManagerd configuration.
|
||||
configuration = if no_configuration
|
||||
Baguette::Log.info "do not load a configuration file."
|
||||
Baguette::Configuration::DNSManager.new
|
||||
else
|
||||
# In case there is a configuration file helping with the parameters.
|
||||
Baguette::Configuration::DNSManager.get(configuration_file) ||
|
||||
Baguette::Configuration::DNSManager.new
|
||||
OptionParser.parse do |parser|
|
||||
parser.on "-v verbosity-level", "--verbosity level", "Verbosity." do |opt|
|
||||
Baguette::Log.info "Verbosity level: #{opt}"
|
||||
configuration.verbosity = opt.to_i
|
||||
end
|
||||
|
||||
|
||||
OptionParser.parse do |parser|
|
||||
parser.on "-v verbosity-level", "--verbosity level", "Verbosity." do |opt|
|
||||
Baguette::Log.info "Verbosity level: #{opt}"
|
||||
configuration.verbosity = opt.to_i
|
||||
end
|
||||
|
||||
# IPC Service options
|
||||
parser.on "-s service_name", "--service_name service_name", "Service name (IPC)." do |service_name|
|
||||
Baguette::Log.info "Service name: #{service_name}"
|
||||
configuration.service_name = service_name
|
||||
end
|
||||
|
||||
parser.on "-r storage_directory", "--root storage_directory", "Storage directory." do |storage_directory|
|
||||
Baguette::Log.info "Storage directory: #{storage_directory}"
|
||||
configuration.storage_directory = storage_directory
|
||||
end
|
||||
|
||||
parser.on "-l login", "--login login", "DNS manager authd login." do |login|
|
||||
Baguette::Log.info "Authd login for dnsmanager: #{login}"
|
||||
configuration.login = login
|
||||
end
|
||||
|
||||
parser.on "-p pass", "--pass pass", "DNS manager authd pass." do |pass|
|
||||
Baguette::Log.info "Authd pass (not echoed)"
|
||||
configuration.pass = pass
|
||||
end
|
||||
|
||||
parser.on "-h", "--help", "Show this help" do
|
||||
puts parser
|
||||
exit 0
|
||||
end
|
||||
# IPC Service options
|
||||
parser.on "-s service_name", "--service_name service_name", "Service name (IPC)." do |service_name|
|
||||
Baguette::Log.info "Service name: #{service_name}"
|
||||
configuration.service_name = service_name
|
||||
end
|
||||
|
||||
unless File.directory? configuration.template_directory
|
||||
Baguette::Log.warning "template directory '#{configuration.template_directory}' doesn't exist"
|
||||
if File.directory? "./templates"
|
||||
Baguette::Log.info "using template directory './templates'"
|
||||
configuration.template_directory = "./templates"
|
||||
else
|
||||
Baguette::Log.error "tried template directory './templates', but doesn't exist either"
|
||||
Baguette::Log.error "no template directory detected, quitting"
|
||||
exit 1
|
||||
end
|
||||
parser.on "-r storage_directory", "--root storage_directory", "Storage directory." do |storage_directory|
|
||||
Baguette::Log.info "Storage directory: #{storage_directory}"
|
||||
configuration.storage_directory = storage_directory
|
||||
end
|
||||
|
||||
dir = configuration.template_directory
|
||||
accepted_domains = configuration.accepted_domains
|
||||
|
||||
unless accepted_domains
|
||||
Baguette::Log.error "Not even a single accepted domain configured. Probably an error."
|
||||
exit 1
|
||||
parser.on "-l login", "--login login", "DNS manager authd login." do |login|
|
||||
Baguette::Log.info "Authd login for dnsmanager: #{login}"
|
||||
configuration.login = login
|
||||
end
|
||||
|
||||
accepted_domains.each do |domain|
|
||||
template_file = "#{dir}/#{domain}.json"
|
||||
zone = DNSManager::Storage::Zone.from_json File.read "#{template_file}"
|
||||
puts "default zone for #{domain}: #{zone}"
|
||||
rescue e
|
||||
Baguette::Log.error "error reading template #{template_file}: #{e}"
|
||||
exit 1
|
||||
parser.on "-p pass", "--pass pass", "DNS manager authd pass." do |pass|
|
||||
Baguette::Log.info "Authd pass (not echoed)"
|
||||
configuration.pass = pass
|
||||
end
|
||||
|
||||
if simulation
|
||||
pp! configuration
|
||||
parser.on "-h", "--help", "Show this help" do
|
||||
puts parser
|
||||
exit 0
|
||||
end
|
||||
|
||||
unless configuration.pass
|
||||
Baguette::Log.error "no pass found"
|
||||
Baguette::Log.error "Should be present in dnsmanager.yml or via command line arguments (-p)"
|
||||
exit 1
|
||||
end
|
||||
|
||||
service = DNSManager::Service.new configuration
|
||||
service.run
|
||||
end
|
||||
|
||||
main
|
||||
unless File.directory? configuration.template_directory
|
||||
Baguette::Log.warning "template directory '#{configuration.template_directory}' doesn't exist"
|
||||
if File.directory? "./templates"
|
||||
Baguette::Log.info "using template directory './templates'"
|
||||
configuration.template_directory = "./templates"
|
||||
else
|
||||
Baguette::Log.error "tried template directory './templates', but doesn't exist either"
|
||||
Baguette::Log.error "no template directory detected, quitting"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
dir = configuration.template_directory
|
||||
accepted_domains = configuration.accepted_domains
|
||||
|
||||
unless accepted_domains
|
||||
Baguette::Log.error "Not even a single accepted domain configured. Probably an error."
|
||||
exit 1
|
||||
end
|
||||
|
||||
accepted_domains.each do |domain|
|
||||
template_file = "#{dir}/#{domain}.json"
|
||||
zone = DNSManager::Storage::Zone.from_json File.read "#{template_file}"
|
||||
puts "default zone for #{domain}: #{zone}"
|
||||
rescue e
|
||||
Baguette::Log.error "error reading template #{template_file}: #{e}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
if simulation
|
||||
pp! configuration
|
||||
exit 0
|
||||
end
|
||||
|
||||
unless configuration.pass
|
||||
Baguette::Log.error "no pass found"
|
||||
Baguette::Log.error "Should be present in dnsmanager.yml or via command line arguments (-p)"
|
||||
exit 1
|
||||
end
|
||||
|
||||
service = DNSManager::Service.new configuration
|
||||
service.run
|
||||
|
|
|
@ -7,7 +7,7 @@ class DNSManager::Request
|
|||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
dnsmanagerd.storage.get_orphan_domains dnsmanagerd, user.uid
|
||||
dnsmanagerd.storage.get_orphan_domains user.uid
|
||||
end
|
||||
end
|
||||
DNSManager.requests << GetOrphanDomains
|
||||
|
@ -59,8 +59,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
udata = dnsmanagerd.storage.get_user_data user
|
||||
return Response::InsufficientRights.new unless udata.admin
|
||||
dnsmanagerd.storage.user_must_be_admin! user.uid
|
||||
dnsmanagerd.storage.generate_all_zonefiles
|
||||
end
|
||||
end
|
||||
|
@ -77,8 +76,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
udata = dnsmanagerd.storage.get_user_data user
|
||||
return Response::InsufficientRights.new unless udata.admin
|
||||
dnsmanagerd.storage.user_must_be_admin! user.uid
|
||||
dnsmanagerd.storage.generate_zonefile @domain
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
require "ipc/json"
|
||||
require "grok"
|
||||
|
||||
class DNSManager::Request
|
||||
|
||||
IPC::JSON.message Migration, 24 do
|
||||
property login : String
|
||||
property domain : String
|
||||
|
||||
def initialize(@login, @domain)
|
||||
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.user_must_be_admin! user.uid
|
||||
|
||||
response = dnsmanagerd.authd.get_user? @login
|
||||
migrated_user = case response
|
||||
when AuthD::Response::User
|
||||
response.user
|
||||
when AuthD::Response::ErrorMustBeAuthenticated
|
||||
Baguette::Log.error "migration: not authenticated to authd"
|
||||
return Response::UnknownUser.new
|
||||
when AuthD::Response::ErrorUserNotFound
|
||||
Baguette::Log.error "migration: user not found in authd"
|
||||
return Response::UnknownUser.new
|
||||
else
|
||||
Baguette::Log.error "migration: unknown error while requesting authd for a user"
|
||||
return Response::UnknownUser.new
|
||||
end
|
||||
|
||||
return Response::UnknownUser.new unless migrated_user
|
||||
|
||||
dnsmanagerd.storage.new_domain migrated_user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << Migration
|
||||
end
|
|
@ -0,0 +1,61 @@
|
|||
class DNSManager::Request
|
||||
IPC::JSON.message AskShareToken, 20 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_share_token user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskShareToken
|
||||
|
||||
IPC::JSON.message AskTransferToken, 21 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_transfer_token user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskTransferToken
|
||||
|
||||
IPC::JSON.message AskUnShareDomain, 22 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_unshare_domain user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskUnShareDomain
|
||||
|
||||
IPC::JSON.message GainOwnership, 23 do
|
||||
property uuid : String
|
||||
|
||||
def initialize(@uuid)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.gain_ownership user.uid, @uuid
|
||||
end
|
||||
end
|
||||
DNSManager.requests << GainOwnership
|
||||
end
|
|
@ -14,13 +14,19 @@ class DNSManager::Request
|
|||
#dnsmanagerd.auth.edit_profile_content user.uid, {
|
||||
# "dnsmanager-last-connection" => JSON::Any.new Time.utc.to_s
|
||||
#}
|
||||
user_data = dnsmanagerd.storage.ensure_user_data response.user.uid
|
||||
return Response::Error.new "invalid user" unless user_data
|
||||
user_id = response.user.uid
|
||||
|
||||
accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil!
|
||||
user_domains = user_data.domains
|
||||
perms = dnsmanagerd.check_permissions response.user.uid, "*"
|
||||
Response::Logged.new (perms == AuthD::User::PermissionLevel::Admin), accepted_domains, user_domains
|
||||
|
||||
# Limit the number of domains in this message.
|
||||
# Pagination will be required beyond a hundred domains.
|
||||
user_domains = dnsmanagerd.storage.user_domains(user_id).[0..100]
|
||||
is_admin = dnsmanagerd.is_admin? user_id
|
||||
|
||||
Response::Logged.new is_admin, accepted_domains, user_domains
|
||||
when AuthD::Response::ErrorUserNotFound
|
||||
Baguette::Log.error "Trying to authenticate an unknown user."
|
||||
Response::ErrorInvalidToken.new
|
||||
else
|
||||
Response::ErrorInvalidToken.new
|
||||
end
|
||||
|
@ -42,7 +48,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.delete_user_data dnsmanagerd, user.uid, user_id
|
||||
dnsmanagerd.storage.delete_user_data user.uid, user_id
|
||||
end
|
||||
end
|
||||
DNSManager.requests << DeleteUser
|
||||
|
|
|
@ -13,9 +13,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil!
|
||||
template_directory = dnsmanagerd.configuration.template_directory
|
||||
dnsmanagerd.storage.new_domain accepted_domains, template_directory, user.uid, @domain
|
||||
dnsmanagerd.storage.new_domain user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << NewDomain
|
||||
|
@ -69,7 +67,8 @@ class DNSManager::Request
|
|||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
dnsmanagerd.storage.user_domains user.uid
|
||||
user_domain_names = dnsmanagerd.storage.user_domains(user.uid).map &.name
|
||||
Response::DomainList.new user_domain_names
|
||||
end
|
||||
end
|
||||
DNSManager.requests << UserDomains
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
class DNSManager::Response
|
||||
IPC::JSON.message DomainDeleted, 9 do
|
||||
property domain : String
|
||||
|
@ -15,9 +14,9 @@ class DNSManager::Response
|
|||
end
|
||||
DNSManager.responses << InvalidZone
|
||||
|
||||
# Domain of a zone cannot change, for security reasons.
|
||||
IPC::JSON.message DomainChanged, 11 do
|
||||
def initialize
|
||||
property domain : DNSManager::Storage::Domain
|
||||
def initialize(@domain)
|
||||
end
|
||||
end
|
||||
DNSManager.responses << DomainChanged
|
||||
|
@ -52,7 +51,7 @@ class DNSManager::Response
|
|||
IPC::JSON.message Logged, 16 do
|
||||
property admin : Bool
|
||||
property accepted_domains : Array(String)
|
||||
property my_domains : Array(String)
|
||||
property my_domains : Array(DNSManager::Storage::Domain)
|
||||
def initialize(@admin, @accepted_domains, @my_domains)
|
||||
end
|
||||
end
|
||||
|
@ -119,4 +118,3 @@ class DNSManager::Response
|
|||
end
|
||||
DNSManager.responses << OrphanDomainList
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
require "ipc"
|
||||
|
||||
require "baguette-crystal-base"
|
||||
require "./config"
|
||||
require "./exceptions"
|
||||
|
||||
class DNSManager::Service < IPC
|
||||
property configuration : Baguette::Configuration::DNSManager
|
||||
getter storage : DNSManager::Storage
|
||||
getter logged_users : Hash(Int32, AuthD::User::Public)
|
||||
|
||||
property authd : AuthD::Client
|
||||
|
||||
def initialize(@configuration)
|
||||
super()
|
||||
@storage = DNSManager::Storage.new @configuration.storage_directory, @configuration.recreate_indexes
|
||||
|
||||
@logged_users = Hash(Int32, AuthD::User::Public).new
|
||||
|
||||
# TODO: auth service isn't in the FDs pool.
|
||||
# If the service crashes, dnsmanagerd won't know it.
|
||||
@authd = AuthD::Client.new
|
||||
response = authd.login? @configuration.login, @configuration.pass.not_nil!
|
||||
case response
|
||||
when AuthD::Response::Login
|
||||
uid = response.uid
|
||||
token = response.token
|
||||
Baguette::Log.info "Authenticated as #{@configuration.login} #{uid}, token: #{token}"
|
||||
else
|
||||
@authd.close
|
||||
raise "Cannot authenticate to authd with login #{@configuration.login}: #{response}."
|
||||
end
|
||||
|
||||
@storage.dnsmanagerd = self
|
||||
self.timer @configuration.ipc_timer
|
||||
self.service_init @configuration.service_name
|
||||
end
|
||||
|
||||
def get_logged_user(event : IPC::Event)
|
||||
@logged_users[event.fd]?
|
||||
end
|
||||
|
||||
def decode_token(token : String)
|
||||
@authd.decode_token token
|
||||
end
|
||||
|
||||
def is_admin?(uid : UInt32) : Bool
|
||||
perms = check_permissions uid, "*"
|
||||
(perms == AuthD::User::PermissionLevel::Admin)
|
||||
end
|
||||
|
||||
def check_permissions(uid : UInt32, resource : String) : AuthD::User::PermissionLevel
|
||||
response = @authd.check_permission uid, "dnsmanager", resource
|
||||
case response
|
||||
when AuthD::Response::PermissionCheck
|
||||
return response.permission
|
||||
end
|
||||
raise CannotCheckPermissionsException.new uid, resource
|
||||
rescue e
|
||||
Baguette::Log.error "error while checking permissions: #{e}"
|
||||
raise CannotCheckPermissionsException.new uid, resource
|
||||
end
|
||||
|
||||
def assert_permissions!(uid : UInt32, resource : String, perms : AuthD::User::PermissionLevel)
|
||||
if check_permissions(uid, resource) < perms
|
||||
raise AuthorizationException.new
|
||||
end
|
||||
end
|
||||
|
||||
def handle_request(event : IPC::Event)
|
||||
request_start = Time.utc
|
||||
|
||||
array = event.message.not_nil!
|
||||
slice = Slice.new array.to_unsafe, array.size
|
||||
message = IPCMessage::TypedMessage.deserialize slice
|
||||
request = DNSManager.requests.parse_ipc_json message.not_nil!
|
||||
|
||||
if request.nil?
|
||||
raise "unknown request type"
|
||||
end
|
||||
|
||||
reqname = request.class.name.sub /^DNSManager::Request::/, ""
|
||||
|
||||
response = begin
|
||||
request.handle self, event
|
||||
rescue e : AuthorizationException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} authorization error"
|
||||
Response::Error.new "authorization error"
|
||||
rescue e : DomainNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} domain not found"
|
||||
Response::DomainNotFound.new
|
||||
rescue e : CannotCheckPermissionsException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'"
|
||||
Response::InsufficientRights.new
|
||||
rescue e : UnknownUserException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} unknown user"
|
||||
Response::UnknownUser.new
|
||||
rescue e : NoOwnershipException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} no ownership error"
|
||||
Response::NoOwnership.new
|
||||
rescue e : NotLoggedException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} user not logged"
|
||||
Response::Error.new "user not logged"
|
||||
rescue e : RRNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR not found"
|
||||
Response::RRNotFound.new
|
||||
rescue e : TokenNotFoundException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} Token not found"
|
||||
Response::Error.new "token not found"
|
||||
rescue e : RRReadOnlyException
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR is read only"
|
||||
Response::RRReadOnly.new e.domain, e.rr
|
||||
rescue e # Generic case
|
||||
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} generic error #{e}"
|
||||
Response::Error.new "generic error"
|
||||
end
|
||||
|
||||
# If clients sent requests with an “id” field, it is copied
|
||||
# in the responses. Allows identifying responses easily.
|
||||
response.id = request.id
|
||||
|
||||
schedule event.fd, response
|
||||
|
||||
duration = Time.utc - request_start
|
||||
|
||||
response_name = response.class.name.sub /^DNSManager::Response::/, ""
|
||||
|
||||
if response.is_a? DNSManager::Response::Error
|
||||
Baguette::Log.warning "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name} (#{response.reason})"
|
||||
else
|
||||
if reqname != "KeepAlive" || @configuration.print_keepalive
|
||||
Baguette::Log.debug "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
Baguette::Log.title "Starting #{@configuration.service_name}"
|
||||
|
||||
self.loop do |event|
|
||||
begin
|
||||
case event.type
|
||||
when LibIPC::EventType::Timer
|
||||
Baguette::Log.debug "Timer." if @configuration.print_ipc_timer
|
||||
|
||||
when LibIPC::EventType::Connection
|
||||
Baguette::Log.debug "New connection!" if @configuration.print_ipc_connection
|
||||
|
||||
when LibIPC::EventType::Disconnection
|
||||
Baguette::Log.debug "Disconnection from #{event.fd}." if @configuration.print_ipc_disconnection
|
||||
@logged_users.delete event.fd
|
||||
|
||||
when LibIPC::EventType::MessageTx
|
||||
Baguette::Log.debug "Message sent to #{event.fd}." if @configuration.print_ipc_message_sent
|
||||
|
||||
when LibIPC::EventType::MessageRx
|
||||
Baguette::Log.debug "Message received from #{event.fd}." if @configuration.print_ipc_message_received
|
||||
handle_request event
|
||||
|
||||
else
|
||||
Baguette::Log.warning "Unhandled IPC event: #{event.class}."
|
||||
if event.responds_to?(:fd)
|
||||
fd = event.fd
|
||||
Baguette::Log.warning "closing #{fd}"
|
||||
close fd
|
||||
@logged_users.delete fd
|
||||
end
|
||||
end
|
||||
|
||||
rescue exception
|
||||
Baguette::Log.error "exception: #{typeof(exception)} - #{exception.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
441
src/storage.cr
441
src/storage.cr
|
@ -2,29 +2,56 @@ require "json"
|
|||
require "uuid"
|
||||
require "uuid/json"
|
||||
require "baguette-crystal-base"
|
||||
require "./service.cr"
|
||||
|
||||
require "dodb"
|
||||
|
||||
class DNSManager::Storage
|
||||
getter user_data : DODB::CachedDataBase(UserData)
|
||||
getter user_data_by_uid : DODB::Index(UserData)
|
||||
getter domains : DODB::Storage::Cached(Domain)
|
||||
getter domains_by_name : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_share_key : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_transfer_key : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_owners : DODB::Trigger::Tags(Domain)
|
||||
|
||||
getter zones : DODB::CachedDataBase(Zone)
|
||||
getter zones_by_domain : DODB::Index(Zone)
|
||||
getter zones : DODB::Storage::Cached(Zone)
|
||||
getter zones_by_domain : DODB::Trigger::IndexCached(Zone)
|
||||
|
||||
getter tokens : DODB::CachedDataBase(Token)
|
||||
getter tokens_by_uuid : DODB::Index(Token)
|
||||
getter tokens_by_domain : DODB::Partition(Token)
|
||||
getter tokens : DODB::Storage::Cached(Token)
|
||||
getter tokens_by_uuid : DODB::Trigger::IndexCached(Token)
|
||||
getter tokens_by_domain : DODB::Trigger::Partition(Token)
|
||||
|
||||
getter root : String
|
||||
getter zonefiledir : String
|
||||
|
||||
property dnsmanagerd : DNSManager::Service? = nil
|
||||
|
||||
def dnsmanagerd() : DNSManager::Service
|
||||
@dnsmanagerd.not_nil!
|
||||
rescue
|
||||
raise Exception.new "dnsmanagerd not defined"
|
||||
end
|
||||
|
||||
def initialize(@root : String, reindex : Bool = false)
|
||||
@user_data = DODB::CachedDataBase(UserData).new "#{@root}/user-data"
|
||||
@user_data_by_uid = @user_data.new_index "uid", &.uid.to_s
|
||||
@zones = DODB::CachedDataBase(Zone).new "#{@root}/zones"
|
||||
@domains = DODB::Storage::Cached(Domain).new "#{@root}/domains"
|
||||
@domains_by_name = @domains.new_index "name", &.name
|
||||
@domains_by_share_key = @domains.new_index "share-key", do |d|
|
||||
if k = d.share_key
|
||||
k
|
||||
else
|
||||
DODB.no_index
|
||||
end
|
||||
end
|
||||
@domains_by_transfer_key = @domains.new_index "transfer-key", do |d|
|
||||
if k = d.transfer_key
|
||||
k
|
||||
else
|
||||
DODB.no_index
|
||||
end
|
||||
end
|
||||
@domains_by_owners = @domains.new_tags "owners", &.owners.map &.to_s
|
||||
@zones = DODB::Storage::Cached(Zone).new "#{@root}/zones"
|
||||
@zones_by_domain = @zones.new_index "domain", &.domain
|
||||
@tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens"
|
||||
@tokens = DODB::Storage::Cached(Token).new "#{@root}/tokens"
|
||||
@tokens_by_uuid = @tokens.new_index "uuid", &.uuid
|
||||
@tokens_by_domain = @tokens.new_partition "domain", &.domain
|
||||
|
||||
|
@ -34,8 +61,8 @@ class DNSManager::Storage
|
|||
Baguette::Log.info "storage initialized"
|
||||
|
||||
if reindex
|
||||
Baguette::Log.debug "Reindexing user data..."
|
||||
@user_data.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing domains..."
|
||||
@domains.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing zones..."
|
||||
@zones.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing tokens..."
|
||||
|
@ -44,73 +71,55 @@ class DNSManager::Storage
|
|||
end
|
||||
end
|
||||
|
||||
def get_user_data(uid : UserDataID)
|
||||
user_data_by_uid.get uid.to_s
|
||||
rescue e : DODB::MissingEntry
|
||||
entry = UserData.new uid
|
||||
entry
|
||||
end
|
||||
# Generate Bind9 zone files.
|
||||
# The file is written in a temporary file then moved, enabling safe manipulation of the file
|
||||
# since its content always will be consistent even if not up-to-date.
|
||||
def generate_bind9_zonefile(domain : String) : Nil
|
||||
zone = zone_must_exist! domain
|
||||
|
||||
def get_user_data(user : ::AuthD::User::Public)
|
||||
get_user_data user.uid
|
||||
end
|
||||
# Safe write.
|
||||
filename_final = "#{@zonefiledir}/#{zone.domain}"
|
||||
filename_wip = "#{filename_final}.wip"
|
||||
Baguette::Log.info "writing zone file #{filename_final}"
|
||||
|
||||
def update_user_data(user_data : UserData)
|
||||
user_data_by_uid.update_or_create user_data.uid.to_s, user_data
|
||||
end
|
||||
|
||||
def ensure_user_data(user_id : UserDataID)
|
||||
user_data = user_data_by_uid.get? user_id.to_s
|
||||
unless user_data
|
||||
Baguette::Log.info "New user #{user_id}"
|
||||
user_data = UserData.new user_id
|
||||
@user_data << user_data
|
||||
File.open(filename_wip, "w") do |file|
|
||||
zone.to_bind9 file
|
||||
end
|
||||
|
||||
user_data
|
||||
# Rename WIP filename to final file name.
|
||||
File.rename filename_wip, filename_final
|
||||
end
|
||||
|
||||
# Request to generate a zone file.
|
||||
# Only an admin can access this function, so there is no need to verify user's authorizations a second time.
|
||||
def generate_zonefile(domain : String) : IPC::JSON
|
||||
generate_bind9_zonefile domain
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
# Only an admin can access this function.
|
||||
def generate_all_zonefiles() : IPC::JSON
|
||||
Baguette::Log.info "writing all zone files in #{@zonefiledir}/"
|
||||
zones.each do |zone|
|
||||
# TODO: safe write.
|
||||
File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file|
|
||||
zone.to_bind9 file
|
||||
end
|
||||
end
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
# Only an admin can access this function.
|
||||
def generate_zonefile(domain : String) : IPC::JSON
|
||||
zone = zone_must_exist! domain
|
||||
|
||||
Baguette::Log.info "writing zone file #{@zonefiledir}/#{zone.domain}"
|
||||
# TODO: safe write.
|
||||
File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file|
|
||||
zone.to_bind9 file
|
||||
generate_bind9_zonefile zone.domain
|
||||
end
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
# Provides the generated zone file to a user.
|
||||
def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, zone.domain
|
||||
user_should_own! user_id, zone.domain
|
||||
|
||||
io = IO::Memory.new
|
||||
zone.to_bind9 io
|
||||
Response::GeneratedZone.new domain, (String.new io.buffer, io.pos)
|
||||
end
|
||||
|
||||
def new_domain(accepted_domains : Array(String),
|
||||
template_directory : String,
|
||||
user_id : UserDataID,
|
||||
domain : String) : IPC::JSON
|
||||
|
||||
user_data = user_must_exist! user_id
|
||||
# Adds a new domain.
|
||||
def new_domain(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil!
|
||||
template_directory = dnsmanagerd.configuration.template_directory
|
||||
|
||||
# Prevent future very confusing errors.
|
||||
domain = domain.downcase
|
||||
|
@ -120,12 +129,14 @@ class DNSManager::Storage
|
|||
# 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}"
|
||||
Baguette::Log.warning "trying to add an unacceptable domain: '#{domain}'"
|
||||
return Response::UnacceptableDomain.new
|
||||
end
|
||||
|
||||
matching_domains.each do |md|
|
||||
Baguette::Log.info "Add new domain #{domain} (matching domain #{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.
|
||||
|
@ -138,23 +149,102 @@ class DNSManager::Storage
|
|||
# Replace domain by the real domain.
|
||||
default_zone.replace_domain domain
|
||||
|
||||
# Reset the serial number so it represents the current date.
|
||||
# This is particularly useful when a zone is deleted then re-used again right after, in this case
|
||||
# the serial would be a problem since the new zone may not be usable for some time, depending on the
|
||||
# configuration.
|
||||
default_zone.reset_serial
|
||||
|
||||
#
|
||||
# Actually write data on-disk.
|
||||
#
|
||||
|
||||
# Add the domain to the user's domain.
|
||||
user_data.domains << domain
|
||||
update_user_data user_data
|
||||
# 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.
|
||||
zones_by_domain.update_or_create domain, default_zone
|
||||
update_zone default_zone
|
||||
|
||||
Response::DomainAdded.new domain
|
||||
end
|
||||
|
||||
def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
# Asks for a "share token".
|
||||
def ask_share_token(user_id : UserDataID, domain_name : String) : IPC::JSON
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.share_key.nil?
|
||||
# Clone the domain so the database doesn't try to remove the new `shared_key`.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.share_key = UUID.random.to_s
|
||||
@domains_by_name.update domain_name, domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "The domain already have a share key."
|
||||
end
|
||||
end
|
||||
|
||||
# Asks for a "transfer token".
|
||||
def ask_transfer_token(user_id : UserDataID, domain_name : String) : IPC::JSON
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.transfer_key.nil?
|
||||
# Clone the domain so the database doesn't try to remove the new `transfer_key`.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.transfer_key = UUID.random.to_s
|
||||
@domains_by_name.update domain_name, domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "The domain already have a transfer key."
|
||||
end
|
||||
end
|
||||
|
||||
# Check the domain owners.
|
||||
# In case there's only the requesting user, allow him to gain full control.
|
||||
def ask_unshare_domain(user_id : UserDataID, domain_name : String) : IPC::JSON
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.owners.size == 1 && domain.owners[0] == user_id
|
||||
# Clone the domain so the old `shared_key` still exists in the old version.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.share_key = nil
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "You are not the only owner."
|
||||
end
|
||||
end
|
||||
|
||||
# Uses either a "share" or a "transfer" token.
|
||||
def gain_ownership(user_id : UserDataID, uuid : String) : IPC::JSON
|
||||
if domain = @domains_by_share_key.get? uuid
|
||||
if domain.owners.includes? user_id
|
||||
return Response::Error.new "You already own this domain."
|
||||
end
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.owners << user_id
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
elsif domain = @domains_by_transfer_key.get? uuid
|
||||
if domain.owners.includes? user_id
|
||||
return Response::Error.new "You already own this domain."
|
||||
end
|
||||
# Clone the domain so the old `transfer_key` still exists in the old version.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.transfer_key = nil
|
||||
domain_cloned.owners = [user_id]
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "There is no key with this UUID."
|
||||
end
|
||||
end
|
||||
|
||||
def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON
|
||||
# Test zone validity.
|
||||
if errors = zone.get_errors?
|
||||
Baguette::Log.warning "zone #{zone.domain} update with errors: #{errors}"
|
||||
|
@ -163,25 +253,20 @@ class DNSManager::Storage
|
|||
|
||||
# Does the zone already exist?
|
||||
if z = zones_by_domain.get? zone.domain
|
||||
user_should_own! user_data, z.domain
|
||||
user_should_own! user_id, z.domain
|
||||
else
|
||||
# Add the domain to the user's domain.
|
||||
user_data.domains << zone.domain
|
||||
|
||||
# Actually write data on-disk.
|
||||
update_user_data user_data
|
||||
domains << Domain.new zone.domain
|
||||
end
|
||||
|
||||
# Add -or replace- the zone.
|
||||
zones_by_domain.update_or_create zone.domain, zone
|
||||
update_zone zone
|
||||
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
def add_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
# Test RR validity.
|
||||
rr.get_errors.tap do |errors|
|
||||
|
@ -200,18 +285,22 @@ class DNSManager::Storage
|
|||
|
||||
# Any modification of the zone must be performed here.
|
||||
# This function updates the SOA serial before storing the modified zone.
|
||||
def update_zone(zone : Zone)
|
||||
# Also, this function generate the Bind9 file.
|
||||
def update_zone(zone : Zone) : Nil
|
||||
zone.update_serial
|
||||
zones_by_domain.update_or_create zone.domain, zone
|
||||
|
||||
generate_bind9_zonefile zone.domain
|
||||
end
|
||||
|
||||
def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
zone.rr_not_readonly! rr.rrid
|
||||
|
||||
zone_clone = zone.clone
|
||||
|
||||
# Test RR validity.
|
||||
rr.get_errors.tap do |errors|
|
||||
unless errors.empty?
|
||||
|
@ -220,17 +309,16 @@ class DNSManager::Storage
|
|||
end
|
||||
end
|
||||
|
||||
zone.update_rr rr
|
||||
zone_clone.update_rr rr
|
||||
|
||||
update_zone zone
|
||||
update_zone zone_clone
|
||||
|
||||
Response::RRUpdated.new domain, rr
|
||||
end
|
||||
|
||||
def delete_rr(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
rr = zone.rr_not_readonly! rrid
|
||||
|
||||
|
@ -239,46 +327,72 @@ class DNSManager::Storage
|
|||
tokens_by_uuid.delete token_uuid
|
||||
end
|
||||
|
||||
zone.delete_rr rrid
|
||||
update_zone zone
|
||||
zone_clone = zone.clone
|
||||
|
||||
zone_clone.delete_rr rrid
|
||||
update_zone zone_clone
|
||||
|
||||
Response::RRDeleted.new rrid
|
||||
end
|
||||
|
||||
def delete_domain(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
|
||||
# Remove this domain from the list of user's domains.
|
||||
user_data.domains.delete domain
|
||||
|
||||
# Update on-disk user data.
|
||||
update_user_data user_data
|
||||
|
||||
# Remove the related zone and their registered tokens.
|
||||
zones_by_domain.delete domain
|
||||
tokens_by_domain.delete domain
|
||||
|
||||
Response::DomainDeleted.new domain
|
||||
# Removes a Bind9 zonefile.
|
||||
def remove_bind9_zonefile(domain : String) : Nil
|
||||
Baguette::Log.info "Removing a Bind9 zone file."
|
||||
File.delete "#{@zonefiledir}/#{domain}"
|
||||
end
|
||||
|
||||
# Get all removed users from `authd`, list all their domains and remove their data from `dnsmanagerd`.
|
||||
def get_orphan_domains(dnsmanagerd : DNSManager::Service, user_id : UserDataID) : IPC::JSON
|
||||
user_must_be_admin! dnsmanagerd, user_id
|
||||
# Deletes a domain.
|
||||
def delete_domain(user_id : UserDataID, domain_name : String) : IPC::JSON
|
||||
zone_must_exist! domain_name
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
# The user isn't the only owner, just remove him from the list of owners.
|
||||
if domain.owners.size > 1
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.owners.select! { |o| o != user_id }
|
||||
@domains_by_name.update domain_cloned
|
||||
|
||||
# Not really a domain deletion, but that'll do the trick.
|
||||
return Response::DomainDeleted.new domain_name
|
||||
end
|
||||
|
||||
# Remove the related zone and their registered tokens.
|
||||
zones_by_domain.delete domain_name
|
||||
|
||||
# The domain may not have tokens.
|
||||
tokens_by_domain.delete? domain_name
|
||||
|
||||
# Remove this domain_name from the list of user's domains.
|
||||
domains_by_name.delete domain_name
|
||||
|
||||
remove_bind9_zonefile domain_name
|
||||
|
||||
Response::DomainDeleted.new domain_name
|
||||
end
|
||||
|
||||
# Gets all removed users from `authd`, list all their domains and remove their data from `dnsmanagerd`.
|
||||
def get_orphan_domains(user_id : UserDataID) : IPC::JSON
|
||||
user_must_be_admin! user_id
|
||||
|
||||
Baguette::Log.debug "list all orphan domains (long computation)"
|
||||
orphans = [] of String
|
||||
user_data.each do |user|
|
||||
begin
|
||||
dnsmanagerd.authd.get_user? user.uid
|
||||
rescue e
|
||||
Baguette::Log.warning "no authd info on user #{user.uid}: #{e} (removing this user)"
|
||||
Baguette::Log.debug "user #{user.uid} had #{user.domains.size} domains"
|
||||
user.domains.each do |domain|
|
||||
orphans << domain
|
||||
domains.each do |domain|
|
||||
domain.owners.each do |owner|
|
||||
begin
|
||||
user_must_exist! owner
|
||||
rescue e
|
||||
Baguette::Log.warning "no authd info on user #{owner}: #{e} (removing this user)"
|
||||
if owned_domains = domains_by_owners.get? owner.to_s
|
||||
Baguette::Log.debug "user #{owner} had #{owned_domains.size} domains"
|
||||
owned_domains.each do |d|
|
||||
orphans << d.name
|
||||
end
|
||||
else
|
||||
Baguette::Log.warning "no domain indexed for user #{owner}, but owns domain #{domain.name}"
|
||||
end
|
||||
wipe_user_data owner
|
||||
end
|
||||
wipe_user_data user
|
||||
end
|
||||
end
|
||||
Baguette::Log.debug "total: #{orphans.size} orphans"
|
||||
|
@ -287,75 +401,95 @@ class DNSManager::Storage
|
|||
end
|
||||
|
||||
def get_zone(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
Response::Zone.new zone
|
||||
end
|
||||
|
||||
def wipe_user_data(user_data : UserData)
|
||||
# Remove the user's domains.
|
||||
user_data.domains.each do |domain|
|
||||
tokens_by_domain.delete domain
|
||||
zones_by_domain.delete domain
|
||||
rescue e
|
||||
Baguette::Log.error "while removing a domain: #{e}"
|
||||
end
|
||||
# Removes user data.
|
||||
def wipe_user_data(user_id : UserDataID) : Nil
|
||||
domains_by_owners.get(user_id.to_s).each do |domain|
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.owners.delete user_id
|
||||
|
||||
# Remove the user.
|
||||
user_data_by_uid.delete user_data.uid.to_s
|
||||
# Remove the user's domain when he is the only owner.
|
||||
if domain_cloned.owners.empty?
|
||||
# The domain may not have tokens.
|
||||
@tokens_by_domain.delete? domain_cloned.name
|
||||
@zones_by_domain.delete domain_cloned.name
|
||||
@domains_by_name.delete domain_cloned.name
|
||||
else
|
||||
@domains_by_name.update domain_cloned
|
||||
end
|
||||
end
|
||||
rescue e
|
||||
Baguette::Log.error "while removing a domain: #{e}"
|
||||
end
|
||||
|
||||
def delete_user_data(dnsmanagerd : DNSManager::Service, user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
user_data_to_delete = if u = user_to_delete
|
||||
user_must_be_admin! dnsmanagerd, user_id
|
||||
# Removes user data.
|
||||
def delete_user_data(user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON
|
||||
user_id_to_delete = if u = user_to_delete
|
||||
user_must_be_admin! user_id
|
||||
Baguette::Log.info "Admin #{user_id} removes data of user #{u}."
|
||||
user_must_exist! u
|
||||
u
|
||||
else
|
||||
Baguette::Log.info "User #{user_data.uid} terminates his account."
|
||||
user_data
|
||||
Baguette::Log.info "User #{user_id} terminates his account."
|
||||
user_id
|
||||
end
|
||||
|
||||
wipe_user_data user_data_to_delete
|
||||
wipe_user_data user_id_to_delete
|
||||
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
def user_domains(user_id : UserDataID) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
Response::DomainList.new user_data.domains
|
||||
# Gets a list of user domains.
|
||||
def user_domains(user_id : UserDataID) : Array(Domain)
|
||||
if doms = domains_by_owners.get? user_id.to_s
|
||||
doms
|
||||
else
|
||||
Array(Domain).new
|
||||
end
|
||||
end
|
||||
|
||||
def user_must_exist!(user_id : UserDataID) : UserData
|
||||
user_data = user_data_by_uid.get? user_id.to_s
|
||||
raise UnknownUserException.new unless user_data
|
||||
user_data
|
||||
# TODO: is the user known from authd?
|
||||
def user_must_exist!(user_id : UserDataID)
|
||||
response = dnsmanagerd().authd.get_user? user_id
|
||||
case response
|
||||
when AuthD::Response::User
|
||||
response.user
|
||||
else
|
||||
raise UnknownUserException.new "user #{user_id} doesn't exist"
|
||||
end
|
||||
end
|
||||
|
||||
def user_must_be_admin!(dnsmanagerd : DNSManager::Service, user_id : UserDataID) : UserData
|
||||
user_data = user_must_exist! user_id
|
||||
dnsmanagerd.assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
|
||||
user_data
|
||||
# Asks `authd` for the user's permissions and verifies the `dnsmanager` permissions are "Admin"-level.
|
||||
def user_must_be_admin!(user_id : UserDataID) : Nil
|
||||
dnsmanagerd().assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
|
||||
end
|
||||
|
||||
# Verifies the existence of a zone.
|
||||
def zone_must_exist!(domain : String) : Zone
|
||||
zone = zones_by_domain.get? domain
|
||||
raise DomainNotFoundException.new unless zone
|
||||
zone
|
||||
end
|
||||
|
||||
def user_should_own!(user_data : UserData, domain : String)
|
||||
unless user_data.domains.includes?(domain) || user_data.admin
|
||||
# Owning a domain means to be in the owners' list of the domain.
|
||||
# TODO: accept admin users to override this test.
|
||||
def user_should_own!(user_id : UserDataID, domain : String) : Nil
|
||||
d = domains_by_name.get? domain
|
||||
raise DomainNotFoundException.new if d.nil?
|
||||
|
||||
unless d.owners.includes? user_id || user_must_be_admin! user_id
|
||||
raise NoOwnershipException.new
|
||||
end
|
||||
end
|
||||
|
||||
# Asks a new token for a resource record.
|
||||
def new_token(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
rr = zone.rr_must_exist! rrid
|
||||
|
||||
|
@ -366,10 +500,11 @@ class DNSManager::Storage
|
|||
|
||||
# 2. add token to the RR.
|
||||
rr.token = token.uuid
|
||||
zone.update_rr rr
|
||||
zone_clone = zone.clone
|
||||
zone_clone.update_rr rr
|
||||
|
||||
# 3. update the zone (no need to call `update_zone` to change the zone serial).
|
||||
zones_by_domain.update_or_create zone.domain, zone
|
||||
zones_by_domain.update zone_clone
|
||||
|
||||
# 4. if there is an old token, remove it.
|
||||
if old_token_uuid = old_token
|
||||
|
@ -382,12 +517,14 @@ class DNSManager::Storage
|
|||
Response::RRUpdated.new domain, rr
|
||||
end
|
||||
|
||||
# Verifies the existence and retrieves a token for automatic updates.
|
||||
def token_must_exist!(token_uuid : String) : Token
|
||||
token = tokens_by_uuid.get? token_uuid
|
||||
raise TokenNotFoundException.new unless token
|
||||
token
|
||||
end
|
||||
|
||||
# Uses a token to automatically update a resource record.
|
||||
def use_token(token_uuid : String, address : String) : IPC::JSON
|
||||
token = token_must_exist! token_uuid
|
||||
zone = zone_must_exist! token.domain
|
||||
|
@ -401,7 +538,7 @@ class DNSManager::Storage
|
|||
return Response::Error.new "invalid ipv4" unless Zone.is_ipv4_address_valid? address
|
||||
rr.target = address
|
||||
zone.update_rr rr
|
||||
zones_by_domain.update_or_create zone.domain, zone
|
||||
update_zone zone
|
||||
Response::Success.new
|
||||
when Zone::AAAA
|
||||
return Response::Error.new "invalid ipv6" unless Zone.is_ipv6_address_valid? address
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
require "json"
|
||||
require "uuid"
|
||||
require "uuid/json"
|
||||
|
||||
class DNSManager::Storage::Domain
|
||||
include JSON::Serializable
|
||||
|
||||
property name : String
|
||||
property share_key : String? = nil
|
||||
property transfer_key : String? = nil
|
||||
|
||||
# Users may have many domains, and a domain may have many owners.
|
||||
property owners = [] of UserDataID
|
||||
|
||||
def initialize(@name, share_key = nil, transfer_key = nil, owners = [] of UserDataID)
|
||||
end
|
||||
|
||||
def_clone
|
||||
end
|
|
@ -7,6 +7,8 @@ class DNSManager::Storage::Token
|
|||
property domain : String
|
||||
property rrid : UInt32
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@domain, @rrid)
|
||||
@uuid = UUID.random.to_s
|
||||
end
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
require "json"
|
||||
require "uuid"
|
||||
require "uuid/json"
|
||||
|
||||
alias UserDataID = UInt32
|
||||
|
||||
class DNSManager::Storage::UserData
|
||||
include JSON::Serializable
|
||||
|
||||
property uid : UserDataID
|
||||
|
||||
# Users may have many domains, and a domain can have many owners.
|
||||
property domains = [] of String
|
||||
|
||||
property admin : Bool = false
|
||||
|
||||
def initialize(@uid)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
alias UserDataID = UInt32
|
|
@ -35,6 +35,8 @@ class DNSManager::Storage::Zone
|
|||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def_clone
|
||||
|
||||
alias Error = String
|
||||
|
||||
# Store a Resource Record: A, AAAA, TXT, PTR, CNAME…
|
||||
|
@ -50,6 +52,7 @@ class DNSManager::Storage::Zone
|
|||
NS: NS,
|
||||
CNAME: CNAME,
|
||||
MX: MX,
|
||||
CAA: CAA,
|
||||
SRV: SRV,
|
||||
|
||||
# Special resource records, which actually are TXT records.
|
||||
|
@ -87,7 +90,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} #{ "%10s" % @rrtype } #{ "%30s" % @target}\n"
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} #{ "%.10s" % @rrtype } #{ "%.30s" % @target}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -96,6 +99,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class SOA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
# Start of Authority
|
||||
property mname : String # Master Name Server for the zone.
|
||||
property rname : String # admin email address john.doe@example.com => john\.doe.example.com
|
||||
|
@ -115,6 +120,16 @@ class DNSManager::Storage::Zone
|
|||
@rrtype = "SOA"
|
||||
end
|
||||
|
||||
# Sets the serial number to the current date.
|
||||
def reset_serial
|
||||
t = Time.local
|
||||
y = t.year
|
||||
m = t.month
|
||||
d = t.day
|
||||
|
||||
@serial = "#{y}#{ "%0.2d" % m}#{ "%0.2d" % d}00".to_u64
|
||||
end
|
||||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{name} #{ttl} SOA (#{mname} #{rname}\n"
|
||||
|
@ -159,6 +174,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class A < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -179,6 +196,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class AAAA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -199,6 +218,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class TXT < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -215,7 +236,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} #{ "%10s" % @rrtype } #{quoted_string @target}\n"
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} #{ "%.10s" % @rrtype } #{quoted_string @target}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -224,6 +245,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class PTR < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -252,6 +275,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class NS < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -268,6 +293,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class CNAME < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -289,7 +316,7 @@ class DNSManager::Storage::Zone
|
|||
# TODO: verifications + print.
|
||||
# baguette.netlib.re. 3600 IN TXT "v=spf1 a mx ip4:<IP> a:mail.baguette.netlib.re ~all"
|
||||
class SPF < ResourceRecord
|
||||
|
||||
def_clone
|
||||
|
||||
# SPF mechanisms are about policy, which comes with the form of a single character:
|
||||
# - '?' means no policy (neutral),
|
||||
|
@ -332,6 +359,8 @@ class DNSManager::Storage::Zone
|
|||
property t : Type # type of modifier
|
||||
property v : String # value
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@t, @v)
|
||||
end
|
||||
|
||||
|
@ -362,6 +391,8 @@ class DNSManager::Storage::Zone
|
|||
INCLUDE # include foreign SPF policy
|
||||
end
|
||||
|
||||
def_clone
|
||||
|
||||
property q : Qualifier = Qualifier::Pass
|
||||
property t : Type # type of mechanism
|
||||
property v : String # value
|
||||
|
@ -444,7 +475,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} SPF "
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} SPF "
|
||||
io << '"'
|
||||
@mechanisms.each do |m|
|
||||
io << m
|
||||
|
@ -484,6 +515,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
# TODO
|
||||
class DKIM < ResourceRecord
|
||||
def_clone
|
||||
enum Version
|
||||
DKIM1
|
||||
end
|
||||
|
@ -508,6 +540,8 @@ class DNSManager::Storage::Zone
|
|||
io << "v=#{v};h=#{h.to_s.downcase};k=#{k.to_s.downcase};p=#{p}"
|
||||
io << ";n=#{n}" unless n == ""
|
||||
end
|
||||
|
||||
def_clone
|
||||
end
|
||||
|
||||
property dkim : DKIMProperties
|
||||
|
@ -532,7 +566,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} DKIM #{split_line dkim.to_s}\n"
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} DKIM #{split_line dkim.to_s}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -542,6 +576,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
# TODO
|
||||
class DMARC < ResourceRecord
|
||||
def_clone
|
||||
enum Version
|
||||
DMARC1
|
||||
end
|
||||
|
@ -551,6 +586,8 @@ class DNSManager::Storage::Zone
|
|||
property mail : String
|
||||
property limit : UInt32? = nil
|
||||
|
||||
def_clone
|
||||
|
||||
def to_s(io : IO)
|
||||
io << "#{mail}"
|
||||
if l = @limit
|
||||
|
@ -596,6 +633,8 @@ class DNSManager::Storage::Zone
|
|||
property rf : Array(ReportFormat)? = nil
|
||||
property ri : UInt32
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@p, @sp, @v, @adkim, @aspf, @pct, @fo, @rua, @ruf, @rf, @ri)
|
||||
end
|
||||
|
||||
|
@ -685,7 +724,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} DMARC #{split_line dmarc.to_s}\n"
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} DMARC #{split_line dmarc.to_s}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -693,7 +732,66 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
end
|
||||
|
||||
class CAA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
enum Tag
|
||||
ISSUE
|
||||
ISSUEWILD
|
||||
IODEF
|
||||
CONTACTEMAIL
|
||||
CONTACTPHONE
|
||||
end
|
||||
|
||||
class CAAProperties
|
||||
include JSON::Serializable
|
||||
|
||||
property flag : UInt8 = 0
|
||||
property tag : Tag = Tag::ISSUE
|
||||
property value : String = ""
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@flag, @tag, @value)
|
||||
end
|
||||
end
|
||||
|
||||
property caa : CAAProperties
|
||||
|
||||
def initialize(@name, @ttl, @target, flag, tag, value)
|
||||
@rrtype = "CAA"
|
||||
@caa = CAAProperties.new flag, tag, value
|
||||
end
|
||||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} CAA "
|
||||
io << "#{ "%.3s" % @caa.flag} #{ "%.15s" % @caa.tag} #{@caa.value}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
io << "#{@name} #{@ttl} IN CAA #{@caa.flag} #{@caa.tag.to_s.downcase} #{@caa.value}\n"
|
||||
end
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
unless Zone.is_subdomain_valid? @name
|
||||
errors << "CAA invalid subdomain: #{@name}"
|
||||
end
|
||||
|
||||
if @ttl < Zone.ttl_limit_min
|
||||
errors << "CAA invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
||||
end
|
||||
|
||||
# TODO: rest of the errors.
|
||||
|
||||
errors
|
||||
end
|
||||
end
|
||||
|
||||
class MX < ResourceRecord
|
||||
def_clone
|
||||
property priority : UInt32 = 10
|
||||
def initialize(@name, @ttl, @target, @priority = 10)
|
||||
@rrtype = "MX"
|
||||
|
@ -701,7 +799,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} MX #{ "%3d" % @priority} #{ "%30s" % @target}\n"
|
||||
io << "#{ "%.30s" % @name} #{ "%6d" % @ttl} MX #{ "%3d" % @priority} #{ "%.30s" % @target}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -729,6 +827,7 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class SRV < ResourceRecord
|
||||
def_clone
|
||||
property port : UInt16
|
||||
property protocol : String = "tcp"
|
||||
property priority : UInt32 = 10
|
||||
|
@ -761,7 +860,7 @@ class DNSManager::Storage::Zone
|
|||
io << "#{ "%3d" % @priority} "
|
||||
io << "#{ "%3d" % @weight} "
|
||||
io << "#{ "%5d" % @port} "
|
||||
io << "#{ "%30s" % @target}\n"
|
||||
io << "#{ "%.30s" % @target}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
|
@ -920,6 +1019,16 @@ class DNSManager::Storage::Zone
|
|||
false
|
||||
end
|
||||
|
||||
# Selects the SOA entry then run the `reset_serial` procedure.
|
||||
def reset_serial
|
||||
@resources.each do |rr|
|
||||
case rr
|
||||
when SOA
|
||||
rr.reset_serial
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# When a new domain is recorded, we load a template which contains a placeholder domain.
|
||||
# `replace_domain` replaces this domain name by the real one in the different (preloaded) RR.
|
||||
# Do not forget the last dot ('.') to get a fully qualified domain name (FQDN).
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
if ARGV.size != 2
|
||||
puts "usage: #{PROGRAM_NAME} dnsmanagerd-bind9-dir powerdns-bind9-dir"
|
||||
exit 0
|
||||
end
|
||||
|
||||
class Context
|
||||
class_property dnsmanagerd_dir : String = ""
|
||||
class_property powerdns_dir : String = ""
|
||||
end
|
||||
|
||||
def copy_domain_files(domain : String) : Nil
|
||||
src = "#{Context.dnsmanagerd_dir}/#{domain}"
|
||||
dest = "#{Context.powerdns_dir}/#{domain}"
|
||||
puts "copying #{src} -> #{dest}"
|
||||
i = File.info src
|
||||
File.copy src, dest
|
||||
rescue e : File::AccessDeniedError
|
||||
puts "You don't have enough rights: #{e}"
|
||||
end
|
||||
|
||||
def run_process(cmd : String, params : Array(String), env : Hash(String, String)) : Nil
|
||||
unless Process.run(cmd, params, env,
|
||||
true # clear environment
|
||||
# input: Process::Redirect::Inherit,
|
||||
# output: Process::Redirect::Inherit,
|
||||
# error: Process::Redirect::Inherit
|
||||
).success?
|
||||
puts "cannot run #{cmd} #{params.join(' ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def pdns_reload(domain : String) : Nil
|
||||
puts "reloading a domain: pdns_control bind-reload-now #{domain}"
|
||||
run_process("pdns_control", [ "bind-reload-now", domain ], { "HOME" => "/" })
|
||||
end
|
||||
|
||||
def update_domain(domain : String) : Nil
|
||||
puts "domain to reload: #{domain}"
|
||||
copy_domain_files domain
|
||||
pdns_reload domain
|
||||
end
|
||||
|
||||
def pdns_add(domain : String) : Nil
|
||||
puts "adding a new domain: pdns_control bind-add-zone #{Context.powerdns_dir}/#{domain}"
|
||||
run_process("pdns_control",
|
||||
[ "bind-add-zone", domain, "#{Context.powerdns_dir}/#{domain}" ],
|
||||
{ "HOME" => "/" })
|
||||
end
|
||||
|
||||
def add_domain(domain : String) : Nil
|
||||
puts "domain to add: #{domain}"
|
||||
copy_domain_files domain
|
||||
pdns_add domain
|
||||
end
|
||||
|
||||
def delete_file(path : String)
|
||||
File.delete path
|
||||
rescue e : File::AccessDeniedError
|
||||
puts "You don't have enough rights: #{e}"
|
||||
end
|
||||
|
||||
def del_domain(domain : String) : Nil
|
||||
puts "domain to delete: #{domain}"
|
||||
delete_file "#{Context.powerdns_dir}/#{domain}"
|
||||
# TODO: pdns_control ???
|
||||
end
|
||||
|
||||
Context.dnsmanagerd_dir = ARGV[0]
|
||||
Context.powerdns_dir = ARGV[1]
|
||||
|
||||
dnsmanagerd_dir_content = Dir.children(Context.dnsmanagerd_dir).select { |d| ! d.ends_with? ".wip" }
|
||||
powerdns_dir_content = Dir.children(Context.powerdns_dir)
|
||||
|
||||
both = dnsmanagerd_dir_content & powerdns_dir_content
|
||||
both.each do |d|
|
||||
i1 = File.info "#{Context.dnsmanagerd_dir}/#{d}"
|
||||
i2 = File.info "#{Context.powerdns_dir}/#{d}"
|
||||
|
||||
if i1.modification_time > i2.modification_time
|
||||
puts "has been modified: #{d}"
|
||||
# Wait for a few minutes before changing anything, to avoid useless reloads.
|
||||
if Time.local > i1.modification_time.shift minutes: 5
|
||||
puts "file was modified more than 5 minutes ago"
|
||||
update_domain d
|
||||
else
|
||||
puts "file has been modified less than 5 minutes ago: do not update yet"
|
||||
end
|
||||
else
|
||||
puts "hasn't been modified: #{d}"
|
||||
end
|
||||
end
|
||||
|
||||
to_add = dnsmanagerd_dir_content - powerdns_dir_content
|
||||
to_add.each { |d| add_domain d }
|
||||
|
||||
to_delete = powerdns_dir_content - dnsmanagerd_dir_content
|
||||
to_delete.each { |d| del_domain d }
|
|
@ -1,5 +1,6 @@
|
|||
require "authd"
|
||||
require "ipc"
|
||||
|
||||
require "../src/client.cr"
|
||||
require "http/server"
|
||||
|
||||
|
|
Loading…
Reference in New Issue