require "ipc" require "baguette-crystal-base" require "./config" require "./exceptions" class Array(T) def contains?(value : T) (self.select { |x| x == value }).size > 0 end end module DNSManager # Select messages to mask in the logs (when everything goes well, of course). # No need to flood the logs with keepalive messages or periodic token use, for instance. enum MESSAGE KEEPALIVE USETOKEN end end alias DNSMESSAGE = DNSManager::MESSAGE 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 should_display?(value : DNSMESSAGE) (@configuration.messages_to_mask.select { |x| x == value }).size == 0 end def should_display?(value : IPCMESSAGE) @configuration.ipc_messages_to_show.contains? value end 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[0..15]}..." 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(fd : Int32) : AuthD::User::Public? @logged_users[fd]? end def get_logged_user(event : IPC::Event) : AuthD::User::Public? get_logged_user 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 # `log_user_info` provides a string composed from either the user # id in case the user was authenticated or the file descriptor of # the connection. def log_user_info(fd : Int32) : String if user = get_logged_user fd "userid #{user.uid}" else "fd #{"%4d" % fd}" 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::/, "" connection_info_str = log_user_info event.fd response = begin request.handle self, event rescue e : AuthorizationException Baguette::Log.error "(#{connection_info_str}) #{request} authorization error" Response::Error.new "authorization error" rescue e : DomainNotFoundException Baguette::Log.error "(#{connection_info_str}) #{request} domain not found" Response::DomainNotFound.new rescue e : CannotCheckPermissionsException Baguette::Log.error "(#{connection_info_str}) #{request} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'" Response::InsufficientRights.new rescue e : UnknownUserException Baguette::Log.error "(#{connection_info_str}) #{request} unknown user" Response::UnknownUser.new rescue e : NoOwnershipException Baguette::Log.error "(#{connection_info_str}) #{request} no ownership error" Response::NoOwnership.new rescue e : NotLoggedException Baguette::Log.error "(#{connection_info_str}) #{request} user not logged" Response::Error.new "user not logged" rescue e : RRNotFoundException Baguette::Log.error "(#{connection_info_str}) #{request} RR not found" Response::RRNotFound.new rescue e : TokenNotFoundException Baguette::Log.error "(#{connection_info_str}) #{request} Token not found" Response::Error.new "token not found" rescue e : RRReadOnlyException Baguette::Log.error "(#{connection_info_str}) #{request} RR is read only" Response::RRReadOnly.new e.domain, e.rr rescue e # Generic case Baguette::Log.error "(#{connection_info_str}) #{request} generic error #{e}" Response::Error.new "generic error" end respname = response.class.name.sub /^DNSManager::Response::/, "" # 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 if response.is_a? DNSManager::Response::Error Baguette::Log.warning "(#{connection_info_str}) (#{duration}) #{request} >> #{response}" else # Different cases where we want to simply avoid logging redundant messages. return if reqname == "KeepAlive" && ! should_display? DNSMESSAGE::KEEPALIVE return if reqname == "UseToken" && respname == "Success" && ! should_display? DNSMESSAGE::USETOKEN Baguette::Log.debug "(#{connection_info_str}) (#{duration}) #{request} >> #{response}" 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 should_display? IPCMESSAGE::TIMER when LibIPC::EventType::Connection Baguette::Log.debug "New connection!" if should_display? IPCMESSAGE::CONNECTION when LibIPC::EventType::Disconnection Baguette::Log.debug "Disconnection from #{event.fd}." if should_display? IPCMESSAGE::DISCONNECTION @logged_users.delete event.fd when LibIPC::EventType::MessageTx Baguette::Log.debug "Message sent to #{event.fd}." if should_display? IPCMESSAGE::TX when LibIPC::EventType::MessageRx Baguette::Log.debug "Message received from #{event.fd}." if should_display? IPCMESSAGE::RX 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