Project

General

Profile

Download (13.6 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/>.
#++

d4b5798a Marc Dequènes (Duck)
# to allow in-place run for test
$: << File.join(File.dirname(__FILE__), "..", "lib")
55a68712 Marc Dequenes
#require 'socket'
157c68c9 Marc Dequenes
require 'tempfile'
55a68712 Marc Dequenes
require 'shellwords'
eb6e0359 Marc Dequènes (Duck)
require 'cyborghood/language'
bc4894ce Marc Dequenes
require 'cyborghood/imap'
c427bfc7 Marc Dequenes
require 'cyborghood/mail'
cb0d68fd Marc Dequènes (Duck)
require 'cyborghood/mail_order'
bc4894ce Marc Dequenes
require 'cyborghood/objects'
4153cce4 Marc Dequenes
require 'cyborghood/services/dns'
3f7a1eee Marc Dequenes
55a68712 Marc Dequenes
#Socket.gethostname

5f013d0a Marc Dequènes (Duck)
dfc48308 Marc Dequenes
module CyborgHood
ccab26de Marc Dequènes (Duck)
module PostmanHome
eb6e0359 Marc Dequènes (Duck)
include CHTranslation
d4b5798a Marc Dequènes (Duck)
bindtextdomain("cyborghood_postman", {:path => Config::L10N_DIR, :charset => "UTF-8"})
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
# not yet ready to be a real Cyborg
class Postman #< Cyborg
eb6e0359 Marc Dequènes (Duck)
include CHTranslation
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
def initialize
# load config
Config.load(self.human_name.downcase)
@config = Config.instance
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
ldap_config = @config.ldap
ldap_config.logger = logger
c8996a8d Marc Dequènes (Duck)
ActiveLdap::Base.setup_connection(ldap_config.marshal_dump)
ccab26de Marc Dequènes (Duck)
# 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?
5f013d0a Marc Dequènes (Duck)
end
ccab26de Marc Dequènes (Duck)
@current_thread = Thread.current
@stop_asap = false
@waiting = false

logger.info "Bot '#{self.human_name}' loaded"
5f013d0a Marc Dequènes (Duck)
end

ccab26de Marc Dequènes (Duck)
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
5f013d0a Marc Dequènes (Duck)
end
ccab26de Marc Dequènes (Duck)
logger.info "Bot was asked to stop..." if @stop_asap
logger.info "Bot terminating"
end
5f013d0a Marc Dequènes (Duck)
c75f6985 Marc Dequènes (Duck)
def ask_to_stop
@stop_asap = true
Thread.critical = true
@current_thread.raise if @waiting
Thread.critical = false
end

private

ccab26de Marc Dequènes (Duck)
def check_mails(imap)
imap.check_mail do |msg|
if @stop_asap
logger.info "Bot was asked to stop..."
break
end
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
mail = Mail.new(msg.content)
logger.info "Received mail with ID '#{mail.message_id}': #{mail.from_addrs} -> #{mail.to_addrs} (#{mail.subject})"
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
# 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
5f013d0a Marc Dequènes (Duck)
next
end
ccab26de Marc Dequènes (Duck)
logger.debug "RFC3156 content detected"
begin
cb0d68fd Marc Dequènes (Duck)
report = mail.process
ccab26de Marc Dequènes (Duck)
rescue CyberError => e
case e.severity
1b7ef95e Marc Dequènes (Duck)
when :grave
cb0d68fd Marc Dequènes (Duck)
logger.fatal "Fatal processing error, exiting (#{e.message})"
ccab26de Marc Dequènes (Duck)
exit 2
when :unrecoverable
logger.error "Internal processing error, skipping mail (#{e.message})"
next
1b7ef95e Marc Dequènes (Duck)
when :processable
logger.error "Untreated processing problem, skipping mail (#{e.message})"
next
ccab26de Marc Dequènes (Duck)
when :ignorable
1b7ef95e Marc Dequènes (Duck)
logger.warn "Internal processing warning, continuing (#{e.message})"
ccab26de Marc Dequènes (Duck)
end
end
cb0d68fd Marc Dequènes (Duck)
result_tag = report.ok? ? "SUCCESS" : "FAILURE"
eb6e0359 Marc Dequènes (Duck)
result_msg = "Processing result: #{result_tag}"
result_msg += " (#{report.error.untranslated})" unless report.ok?
logger.info result_msg
cb0d68fd Marc Dequènes (Duck)
83a04d84 Marc Dequènes (Duck)
i18n = I18n.instance
i18n.set_language_for_user(report.user)
5f013d0a Marc Dequènes (Duck)
cb0d68fd Marc Dequènes (Duck)
unless report.ok?
if report.warn_sender
ccab26de Marc Dequènes (Duck)
logger.info "Sending reply for rejected message"
eb6e0359 Marc Dequènes (Duck)
reply_intro = report.user ? _("Hello %{cn},", :cn =>report.user.cn) : _("Hello,")
mail_reply = mail.create_simple_reject_reply(reply_intro.to_s + "\n\n" +
_("A message (ID: %{id}), apparently from you, was rejected for the following reason:",
:id => mail.message_id).to_s + "\n " + report.error.to_s + "\n" + mail_signature())
ccab26de Marc Dequènes (Duck)
mail_reply.deliver
end
msg.delete
next
5f013d0a Marc Dequènes (Duck)
end

cb0d68fd Marc Dequènes (Duck)
order = MailOrder.parse(report.user, report.message)
result_tag = order.valid? ? "SUCCESS" : "FAILURE"
eb6e0359 Marc Dequènes (Duck)
result_msg = "Processing result: #{result_tag}"
result_msg += " (#{order.error.untranslated})" unless order.valid?
logger.info result_msg

reply_intro = _("Hello %{cn},", :cn => order.user.cn)
cb0d68fd Marc Dequènes (Duck)
unless order.valid?
logger.info "Sending reply for rejected order"
eb6e0359 Marc Dequènes (Duck)
mail_reply = mail.create_simple_reject_reply(reply_intro.to_s + "\n\n" +
_("An order, in a message (ID: %{id}) from you, was rejected for the following reason:",
:id => mail.message_id).to_s + "\n " + order.error.to_s + "\n" + mail_signature())
cb674fb0 Marc Dequènes (Duck)
mail_reply.sign_and_crypt(order.user.keyFingerPrint)
cb0d68fd Marc Dequènes (Duck)
mail_reply.deliver
msg.delete
next
end

ccab26de Marc Dequènes (Duck)
logger.debug "Message accepted, processing orders..."
f0a75e9c Marc Dequènes (Duck)
result_list = CommandRunner.run(order)
ccab26de Marc Dequènes (Duck)
# create transcript
logger.debug "Preparing reply"
eb6e0359 Marc Dequènes (Duck)
reply_txt = reply_intro.to_s + "\n\n"
reply_txt += _("Follows the transcript of your commands:").to_s + "\n"
ccab26de Marc Dequènes (Duck)
reply_attachments = []
result_list.each do |result|
7d701a64 Marc Dequènes (Duck)
reply_txt += "> #{result.cmd}\n"
reply_txt += "#{result.message}\n"
ccab26de Marc Dequènes (Duck)
reply_attachments += result.refs unless result.refs.nil?
end
7d701a64 Marc Dequènes (Duck)
reply_txt += "\n" + mail_signature()
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
# 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 = []
5f013d0a Marc Dequènes (Duck)
p = CyborgHood::Mail.new
ccab26de Marc Dequènes (Duck)
transcript_part = p
5f013d0a Marc Dequènes (Duck)
mail_reply.parts << p
ccab26de Marc Dequènes (Duck)
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
5f013d0a Marc Dequènes (Duck)
end
ccab26de Marc Dequènes (Duck)
# 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
5f013d0a Marc Dequènes (Duck)
end
end

c75f6985 Marc Dequènes (Duck)
def mail_signature
s = "\n" +
"-- \n" +
"#{CyborgHood::PRODUCT} v#{CyborgHood::VERSION}\n"
eb6e0359 Marc Dequènes (Duck)
s += _("Contact eMail:").to_s + " \"#{@config.contact.name}\" <#{@config.contact.email}>\n" if @config.contact.email
s += _("Contact URL:").to_s + " #{@config.contact.url}\n" if @config.contact.url
c75f6985 Marc Dequènes (Duck)
s
ccab26de Marc Dequènes (Duck)
end
5f013d0a Marc Dequènes (Duck)
end

f0a75e9c Marc Dequènes (Duck)
class CommandRunner
eb6e0359 Marc Dequènes (Duck)
include CHTranslation
ccab26de Marc Dequènes (Duck)
def self.run(order)
result_list = []
f0a75e9c Marc Dequènes (Duck)
order.commands.each do |cmd|
7d701a64 Marc Dequènes (Duck)
result = OpenStruct.new
result.cmd = cmd.cmdline
result.ok = false

cb0d68fd Marc Dequènes (Duck)
if cmd.valid?
logger.info "Executing command: #{cmd.cmdline}"
begin
7d701a64 Marc Dequènes (Duck)
execute_cmd(order.user, cmd.cmdsplit, order.shared_parameters, result)
cb0d68fd Marc Dequènes (Duck)
rescue CyberError => e
result.message = e.message.capitalize + "."
rescue
eb6e0359 Marc Dequènes (Duck)
logger.error "Command crashed: " + $!
logger.error "Crash trace: " + $!.backtrace.join("\n")
cb0d68fd Marc Dequènes (Duck)
result.message = _("Internal error. Administrator is warned.")
ccab26de Marc Dequènes (Duck)
end
cb0d68fd Marc Dequènes (Duck)
eb6e0359 Marc Dequènes (Duck)
tag = result.ok ? "SUCCESS" : "FAILURE"
cb0d68fd Marc Dequènes (Duck)
logger.debug "Command result: [#{tag}] #{result.message}"
else
7d701a64 Marc Dequènes (Duck)
logger.info "Invalid command detected: #{cmd.cmdline}"
eb6e0359 Marc Dequènes (Duck)
cmd.parsing_errors.collect{|err| logger.debug "Invalid command detected - reason: " + err.untranslated }
result.message = cmd.parsing_errors.collect{|err| err.to_s }.join("\n")
dfc48308 Marc Dequenes
end
ccab26de Marc Dequènes (Duck)
result_list << result
end
result_list
3f7a1eee Marc Dequenes
end
55a68712 Marc Dequenes
ccab26de Marc Dequènes (Duck)
private
dfc48308 Marc Dequenes
7d701a64 Marc Dequènes (Duck)
def self.execute_cmd(user, cmdline, shared_parameters, result)
ccab26de Marc Dequènes (Duck)
subsys = cmdline.shift
case subsys.upcase
when "DNS"
dfc48308 Marc Dequenes
return if cmdline.empty?
case cmdline.shift.upcase
ccab26de Marc Dequènes (Duck)
when "INFO"
return unless cmdline.empty?
list = CyborgHood::DnsDomain.find_by_manager(user)
txt_list = list.collect{|z| z.cn }.sort.join(", ")
result.ok = true
eb6e0359 Marc Dequènes (Duck)
result.message = _("You are manager of the following zones: %{zone_list}.", :zone_list => txt_list)
ccab26de Marc Dequènes (Duck)
when "GET"
dfc48308 Marc Dequenes
return if cmdline.empty?
ccab26de Marc Dequènes (Duck)
case cmdline.shift.upcase
when "ZONE"
return if cmdline.empty?
zone = cmdline.shift.downcase

dom = CyborgHood::DnsDomain.new(zone)
unless dom.hosted?
66128169 Marc Dequènes (Duck)
result.message = _("This zone is not hosted here.")
ccab26de Marc Dequènes (Duck)
return result
end
unless dom.managed_by? user
result.message = _("You are not allowed to manage this zone.")
return result
end

srv_dns = CyborgHood::Services::DNS.new(zone)
result.ok = true
result.message = _("Requested zone content attached.")
zone_ref = {:content => srv_dns.read_zone, :filename => "dnszone_#{zone}.txt"}.to_ostruct
result.refs = [zone_ref]
dfc48308 Marc Dequenes
end
ccab26de Marc Dequènes (Duck)
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?
result.message = _("This zone is not hosted here.")
return result
end
unless dom.managed_by? user
result.message = _("You are not allowed to manage this zone.")
return result
end
srv_dns = CyborgHood::Services::DNS.new(zone)

return if cmdline.empty?
f0a75e9c Marc Dequènes (Duck)
content_ref = cmdline.shift
cb0d68fd Marc Dequènes (Duck)
part = shared_parameters[content_ref]
f0a75e9c Marc Dequènes (Duck)
unless part.type == "text/plain"
ccab26de Marc Dequènes (Duck)
result.message = _("Attachment has wrong content-type.")
return result
end
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
f = Tempfile.new(zone)
f0a75e9c Marc Dequènes (Duck)
f.write(part.content)
ccab26de Marc Dequènes (Duck)
f.close
logger.debug "Created temporary zone file '#{f.path}'"
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
srv_dns = CyborgHood::Services::DNS.new(zone)
current_serial = srv_dns.serial
logger.debug "Current serial: #{current_serial}"
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
dns_result = srv_dns.check_zone_file(f.path)
unless dns_result.ok
result.message = _("Invalid zone data.")
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
result.message = _("Zone serial is not superior to current serial.")
f.close!
return result
end
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
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
result.message = _("Zone updated.")
else
logger.warn "zone reload failed, replacing old content"
srv_dns.replace_zone_with_backup
result.message = _("Internal error. Administrator is warned.")
end
rescue
logger.warn "Writing zone file failed"
raise
ensure
f.close!
157c68c9 Marc Dequenes
end
end
end
end
55a68712 Marc Dequenes
ccab26de Marc Dequènes (Duck)
if result.message.nil?
# here fall lost souls
7d701a64 Marc Dequènes (Duck)
result.message = _("Command not recognized.")
ccab26de Marc Dequènes (Duck)
end
dfc48308 Marc Dequenes
end
55a68712 Marc Dequenes
end
end
end

ccab26de Marc Dequènes (Duck)
bot = CyborgHood::PostmanHome::Postman.new
55a68712 Marc Dequenes
trap('INT') do
bot.ask_to_stop
end
trap('TERM') do
bot.ask_to_stop
end

bot.run