Project

General

Profile

Download (12.1 KB) Statistics
| Branch: | Tag: | Revision:
55a68712 Marc Dequenes
#!/usr/bin/ruby -Ku

d32ee48a Marc Dequenes
#--
# CyborgHood, a distributed system management software.
# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++

55a68712 Marc Dequenes
# http://www.ruby-doc.org/stdlib/libdoc/net/imap/rdoc/index.html

e9eb9974 Marc Dequènes (Duck)
APP_ROOT = File.dirname(File.expand_path(__FILE__))
Dir.chdir(APP_ROOT)
55a68712 Marc Dequenes
$: << "./lib"

#require 'socket'
157c68c9 Marc Dequenes
require 'tempfile'
55a68712 Marc Dequenes
require 'shellwords'
bc4894ce Marc Dequenes
require 'cyborghood/imap'
c427bfc7 Marc Dequenes
require 'cyborghood/mail'
bc4894ce Marc Dequenes
require 'cyborghood/objects'
4153cce4 Marc Dequenes
require 'cyborghood/services/dns'
fba691ae Marc Dequenes
require 'fileutils'
3f7a1eee Marc Dequenes
55a68712 Marc Dequenes
#Socket.gethostname

5f013d0a Marc Dequènes (Duck)
dfc48308 Marc Dequenes
module CyborgHood
5f013d0a Marc Dequènes (Duck)
# not yet ready to be a real Cyborg
class Postman #< Cyborg
include GetText
bindtextdomain(PRODUCT + "_" + self.human_name, {:path => File.join(APP_ROOT, "locale"), :charset => "UTF-8"})
textdomain(PRODUCT + "_" + self.human_name)

def initialize
# load config
Config.load(self.human_name.downcase)
@config = Config.instance

ldap_config = @config.ldap
ldap_config.logger = logger
ActiveLdap::Base.establish_connection(ldap_config.marshal_dump)

# setup logs
unless @config.log.nil?
logger.output_level(@config.log.console_level) unless @config.log.console_level.nil?
logger.log_to_file(@config.log.file) unless @config.log.file.nil?
end

@current_thread = Thread.current
@stop_asap = false
@waiting = false

logger.info "Bot '#{self.human_name}' loaded"
end

def run
imap = IMAP.new(@config.imap)
until @stop_asap
t = Time.now.to_i
logger.debug "Starting mail check"
check_mails(imap)
logger.debug "Mail check finished"
t2 = Time.now.to_i
sleep_time = @config.imap.min_check_interval - (t2 - t)
if sleep_time > 0
logger.debug "Having a break before new check..."
@waiting = true
begin
sleep(sleep_time)
rescue
end
@waiting = false
end
end
logger.info "Bot was asked to stop..." if @stop_asap
logger.info "Bot terminating"
end

def check_mails(imap)
imap.check_mail do |msg|
if @stop_asap
logger.info "Bot was asked to stop..."
break
end

mail = Mail.new(msg.content)
logger.info "Received mail with ID '#{mail.message_id}': #{mail.from_addrs} -> #{mail.to_addrs} (#{mail.subject})"

# ignore mails not signed or encrypted
unless mail.is_pgp_signed? or mail.is_pgp_encrypted?
logger.info "Mail not signed/encrypted or not RFC3156 compliant, ignoring..."
msg.delete
next
end

logger.debug "RFC3156 content detected"
begin
order = mail.parse
rescue CyberError => e
case e.severity
when :dangerous
logger.fatal " (#{e.message})"
exit 2
when :unrecoverable
logger.error "Internal processing error, skipping mail (#{e.message})"
next
when :ignorable
end
end
result_tag = order.ok ? "SUCCESS" : "FAILURE"
logger.info "Processing result: #{result_tag} (#{order.message})"
logger.info "Extra processing information: " + order.system_message if order.system_message

if order.user
if order.user.preferredLanguage
logger.debug "User preference for langage: " + order.user.preferredLanguage
set_locale(order.user.preferredLanguage)
else
logger.debug "No user preference for langage, using english"
set_locale("en")
end
else
set_locale("en")
end

unless order.ok
if order.warn_sender
logger.info "Sending reply for rejected message"
mail_reply = mail.create_simple_reject_reply("A message (ID: #{mail.message_id}), apparently from you," +
" was rejected for the following reason:\n #{_(order.message)}")
mail_reply.deliver
end
msg.delete
next
end

logger.debug "Message accepted, processing orders..."
result_list = CommandParser.run(order)

# create transcript
logger.debug "Preparing reply"
reply_txt = sprintf(_("Hello %s,"), order.user.cn) + "\n\n"
reply_txt += _(order.message) + "\n\n" if order.message
reply_txt += _("Follows the transcript of your commands:") + "\n"
reply_attachments = []
result_list.each do |result|
reply_txt << "> #{result.cmd}\n"
reply_txt << "#{result.message}\n"
reply_attachments += result.refs unless result.refs.nil?
end

# create mail
logger.debug "Preparing mail"
mail_reply = mail.create_reply
if reply_attachments.empty?
transcript_part = mail_reply
else
mail_reply.set_content_type("multipart", "mixed", {'boundary' => TMail.new_boundary})
parts = []

p = CyborgHood::Mail.new
transcript_part = p
mail_reply.parts << p

reply_attachments.each do |attachment|
p = CyborgHood::Mail.new
p.set_content_type("text", "plain", {'charset' => "utf-8"})
p.set_disposition("attachment", {'filename' => attachment.filename})
p.quoted_printable_body = attachment.content
mail_reply.parts << p
end
end
# insert transcript
transcript_part.set_content_type("text", "plain", {'charset' => 'utf-8', 'format' => 'flowed'})
transcript_part.set_disposition("inline")
transcript_part.quoted_printable_body = reply_txt

# send reply
logger.debug "Sending mail"
mail_reply.sign_and_crypt(order.user.keyFingerPrint)
mail_reply.deliver

logger.info "Message processed completely, deleting"
msg.delete
end
end

def ask_to_stop
@stop_asap = true
Thread.critical = true
@current_thread.raise if @waiting
Thread.critical = false
end
end

dfc48308 Marc Dequenes
class CommandParser
ebccdcc4 Marc Dequènes (Duck)
include GetText
5f013d0a Marc Dequènes (Duck)
textdomain(PRODUCT + "_Postman")
ebccdcc4 Marc Dequènes (Duck)
dfc48308 Marc Dequenes
def self.run(order)
result_list = []
order.commands.each do |cmdstr|
logger.info "Executing command: #{cmdstr}"
begin
e9eb9974 Marc Dequènes (Duck)
result = execute_cmd(order.user, cmdstr, order.references)
dfc48308 Marc Dequenes
if result.nil?
result = OpenStruct.new
result.cmd = cmdstr
result.ok = false
7cae0102 Marc Dequènes (Duck)
result.message = _("Command not recognized.")
dfc48308 Marc Dequenes
result.refs = nil
end
rescue CyberError => e
result = OpenStruct.new
result.cmd = cmdstr
result.ok = false
result.message = e.message.capitalize + "."
result.refs = nil
rescue
logger.warn "Command crashed: " + $!
result = OpenStruct.new
result.cmd = cmdstr
result.ok = false
7cae0102 Marc Dequènes (Duck)
result.message = _("Internal error. Administrator is warned.")
dfc48308 Marc Dequenes
result.refs = nil
end

tag = result.ok ? "SUCCESS" :"FAILURE"
logger.debug "Command result: [#{tag}] #{result.message}"
result_list << result
3f7a1eee Marc Dequenes
end
dfc48308 Marc Dequenes
result_list
55a68712 Marc Dequenes
end

dfc48308 Marc Dequenes
private
55a68712 Marc Dequenes
dfc48308 Marc Dequenes
def self.execute_cmd(user, cmdstr, refs)
cmdline = Shellwords.shellwords(cmdstr)
subsys = cmdline.shift
55a68712 Marc Dequenes
dfc48308 Marc Dequenes
result = OpenStruct.new
result.cmd = cmdstr
result.ok = false
ok = true
case subsys.upcase
when "DNS"
return if cmdline.empty?
case cmdline.shift.upcase
when "INFO"
return unless cmdline.empty?
c427bfc7 Marc Dequenes
list = CyborgHood::DnsDomain.find_by_manager(user)
4153cce4 Marc Dequenes
txt_list = list.collect{|z| z.cn }.sort.join(", ")
dfc48308 Marc Dequenes
result.ok = true
e9eb9974 Marc Dequènes (Duck)
result.message = sprintf(_("You are manager of the following zones: %s."), txt_list)
dfc48308 Marc Dequenes
when "GET"
return if cmdline.empty?
case cmdline.shift.upcase
when "ZONE"
return if cmdline.empty?
zone = cmdline.shift.downcase

dom = CyborgHood::DnsDomain.new(zone)
unless dom.hosted?
7cae0102 Marc Dequènes (Duck)
result.message = ("This zone is not hosted here.")
dfc48308 Marc Dequenes
return result
end
unless dom.managed_by? user
7cae0102 Marc Dequènes (Duck)
result.message = _("You are not allowed to manage this zone.")
dfc48308 Marc Dequenes
return result
591ec1a2 Marc Dequenes
end
dfc48308 Marc Dequenes
srv_dns = CyborgHood::Services::DNS.new(zone)
result.ok = true
7cae0102 Marc Dequènes (Duck)
result.message = _("Requested zone content attached.")
dfc48308 Marc Dequenes
zone_ref = {:content => srv_dns.read_zone, :filename => "dnszone_#{zone}.txt"}.to_ostruct
result.refs = [zone_ref]
591ec1a2 Marc Dequenes
end
dfc48308 Marc Dequenes
when "SET"
return if cmdline.empty?
case cmdline.shift.upcase
when "ZONE"
return if cmdline.empty?
zone = cmdline.shift.downcase
dom = CyborgHood::DnsDomain.new(zone)
unless dom.hosted?
7cae0102 Marc Dequènes (Duck)
result.message = _("This zone is not hosted here.")
dfc48308 Marc Dequenes
return result
end
unless dom.managed_by? user
7cae0102 Marc Dequènes (Duck)
result.message = _("You are not allowed to manage this zone.")
dfc48308 Marc Dequenes
return result
end
srv_dns = CyborgHood::Services::DNS.new(zone)

return if cmdline.empty?
content_ref = cmdline.shift.downcase
return unless content_ref =~ /^@(\d+)$/
part_ref = $1.to_i
unless (1..refs.size).include? part_ref
7cae0102 Marc Dequènes (Duck)
result.message = _("Attachment number not found.")
dfc48308 Marc Dequenes
return result
end
part = refs[part_ref]
unless part.content_type == "text/plain"
7cae0102 Marc Dequènes (Duck)
result.message = _("Attachment has wrong content-type.")
dfc48308 Marc Dequenes
return result
end

f = Tempfile.new(zone)
f.write(part.body)
f.close
logger.debug "Created temporary zone file '#{f.path}'"

srv_dns = CyborgHood::Services::DNS.new(zone)
current_serial = srv_dns.serial
logger.debug "Current serial: #{current_serial}"

dns_result = srv_dns.check_zone_file(f.path)
unless dns_result.ok
7cae0102 Marc Dequènes (Duck)
result.message = _("Invalid zone data.")
dfc48308 Marc Dequenes
f.close!
return result
end
logger.debug "New serial: #{dns_result.serial}"
# allow new serial or missing serial (to allow creating a new zone or replacing a broken zone)
unless current_serial.nil? or dns_result.serial > current_serial
7cae0102 Marc Dequènes (Duck)
result.message = _("Zone serial is not superior to current serial.")
dfc48308 Marc Dequenes
f.close!
return result
end

begin
srv_dns.write_zone_from_file(f.path)
logger.debug "zone changed"
if srv_dns.reload_zone
logger.debug "zone reloaded"
result.ok = true
7cae0102 Marc Dequènes (Duck)
result.message = _("Zone updated.")
157c68c9 Marc Dequenes
else
dfc48308 Marc Dequenes
logger.warn "zone reload failed, replacing old content"
srv_dns.replace_zone_with_backup
7cae0102 Marc Dequènes (Duck)
result.message = _("Internal error. Administrator is warned.")
157c68c9 Marc Dequenes
end
dfc48308 Marc Dequenes
rescue
logger.warn "Writing zone file failed"
raise
ensure
f.close!
157c68c9 Marc Dequenes
end
end
end
55a68712 Marc Dequenes
end

dfc48308 Marc Dequenes
if result.message.nil?
# here fall lost souls
nil
else
result
end
55a68712 Marc Dequenes
end
end
end

bot = CyborgHood::Postman.new

trap('INT') do
bot.ask_to_stop
end
trap('TERM') do
bot.ask_to_stop
end

bot.run