# require "http/server" require "option_parser" require "ipc" require "ipc/json" require "authd" require "baguette-crystal-base" require "./config" module DNSManager class Exception < ::Exception end class AuthorizationException < ::Exception end class NotLoggedException < ::Exception end class AdminAuthorizationException < ::Exception end end require "./storage.cr" require "./network.cr" 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 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::/, "" if reqname != "KeepAlive" || @configuration.print_keepalive Baguette::Log.debug "<< #{reqname}" end response = begin request.handle self, event rescue e : AuthorizationException Baguette::Log.error "#{reqname} authorization error" Response::Error.new "authorization error" rescue e : AdminAuthorizationException Baguette::Log.error "#{reqname} no admin authorization" Response::Error.new "admin authorization error" rescue e : NotLoggedException Baguette::Log.error "#{reqname} user not logged" Response::Error.new "user not logged" rescue e # Generic case Baguette::Log.error "#{reqname} generic error #{e}" DNSManager::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 ">> #{response_name} (#{response.reason}) (Total duration: #{duration})" else if reqname != "KeepAlive" || @configuration.print_keepalive Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" 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 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 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 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 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 end main