Project

General

Profile

Download (13.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
ccab26de Marc Dequènes (Duck)
module PostmanHome
5f013d0a Marc Dequènes (Duck)
include GetText
ccab26de Marc Dequènes (Duck)
bindtextdomain("cyborghood_postman", {:path => File.join(APP_ROOT, "locale"), :charset => "UTF-8"})
5f013d0a Marc Dequènes (Duck)
ccab26de Marc Dequènes (Duck)
# not yet ready to be a real Cyborg
class Postman #< Cyborg
include GetText
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
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?
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
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
5f013d0a Marc Dequènes (Duck)
else
set_locale("en")
end

ccab26de Marc Dequènes (Duck)
unless order.ok
if order.warn_sender
logger.info "Sending reply for rejected message"
c75f6985 Marc Dequènes (Duck)
mail_reply = mail.create_simple_reject_reply(_("Hello,") + "\n\n" + sprintf(
ccab26de Marc Dequènes (Duck)
_("A message (ID: %s), apparently from you, was rejected for the following reason:"),
c75f6985 Marc Dequènes (Duck)
mail.message_id) + "\n " + _(order.message) + "\n" + mail_signature())
ccab26de Marc Dequènes (Duck)
mail_reply.deliver
end
msg.delete
next
5f013d0a Marc Dequènes (Duck)
end

ccab26de Marc Dequènes (Duck)
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
c75f6985 Marc Dequènes (Duck)
reply_txt << 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"
s += _("Contact eMail:") + " \"#{@config.contact.name}\" <#{@config.contact.email}>\n" if @config.contact.email
s += _("Contact URL:") + " #{@config.contact.url}\n" if @config.contact.url
s
ccab26de Marc Dequènes (Duck)
end
5f013d0a Marc Dequènes (Duck)
end

ccab26de Marc Dequènes (Duck)
class CommandParser
include GetText

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

ccab26de Marc Dequènes (Duck)
tag = result.ok ? "SUCCESS" :"FAILURE"
logger.debug "Command result: [#{tag}] #{result.message}"
result_list << result
end
result_list
3f7a1eee Marc Dequenes
end
55a68712 Marc Dequenes
ccab26de Marc Dequènes (Duck)
private
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
def self.execute_cmd(user, cmdstr, refs)
cmdline = Shellwords.shellwords(cmdstr)
subsys = cmdline.shift
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
result = OpenStruct.new
result.cmd = cmdstr
result.ok = false
ok = true
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
result.message = sprintf(_("You are manager of the following zones: %s."), txt_list)
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?
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)
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?
content_ref = cmdline.shift.downcase
return unless content_ref =~ /^@(\d+)$/
part_ref = $1.to_i
unless (1..refs.size).include? part_ref
result.message = _("Attachment number not found.")
return result
end
part = refs[part_ref]
unless part.content_type == "text/plain"
result.message = _("Attachment has wrong content-type.")
return result
end
dfc48308 Marc Dequenes
ccab26de Marc Dequènes (Duck)
f = Tempfile.new(zone)
f.write(part.body)
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
nil
else
result
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