Project

General

Profile

Download (13.6 KB) Statistics
| Branch: | Tag: | Revision:

root / bin / postman @ 44648f83

1
#!/usr/bin/ruby -Ku
2

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

    
21
# to allow in-place run for test
22
$: << File.join(File.dirname(__FILE__), "..", "lib")
23

    
24
#require 'socket'
25
require 'tempfile'
26
require 'shellwords'
27
require 'cyborghood'
28
require 'cyborghood/imap'
29
require 'cyborghood/mail'
30
require 'cyborghood/mail_order'
31
require 'cyborghood/objects'
32
require 'cyborghood/services/dns'
33

    
34
#Socket.gethostname
35

    
36

    
37
module CyborgHood
38
  module PostmanHome
39
    include I18nTranslation
40
    bindtextdomain("cyborghood_postman", {:path => Config::L10N_DIR, :charset => "UTF-8"})
41

    
42
    # not yet ready to be a real Cyborg
43
    class Postman #< Cyborg
44
      include I18nTranslation
45

    
46
      def initialize
47
        # load config
48
        Config.load(self.human_name.downcase)
49
        @config = Config.instance
50

    
51
        ldap_config = @config.ldap
52
        ldap_config.logger = logger
53
        ActiveLdap::Base.setup_connection(ldap_config.marshal_dump)
54

    
55
        # setup logs
56
        unless @config.log.nil?
57
          logger.output_level(@config.log.console_level) unless @config.log.console_level.nil?
58
          logger.log_to_file(@config.log.file) unless @config.log.file.nil?
59
        end
60

    
61
        @current_thread = Thread.current
62
        @stop_asap = false
63
        @waiting = false
64

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

    
68
      def run
69
        imap = IMAP.new(@config.imap)
70
        until @stop_asap
71
          t = Time.now.to_i
72
          logger.debug "Starting mail check"
73
          check_mails(imap)
74
          logger.debug "Mail check finished"
75
          t2 = Time.now.to_i
76
          sleep_time = @config.imap.min_check_interval - (t2 - t)
77
          if sleep_time > 0
78
            logger.debug "Having a break before new check..."
79
            @waiting = true
80
            begin
81
              sleep(sleep_time)
82
            rescue
83
            end
84
            @waiting = false
85
          end
86
        end
87
        logger.info "Bot was asked to stop..." if @stop_asap
88
        logger.info "Bot terminating"
89
      end
90

    
91
      def ask_to_stop
92
        @stop_asap = true
93
        Thread.critical = true
94
        @current_thread.raise if @waiting
95
        Thread.critical = false
96
      end
97

    
98
      private
99

    
100
      def check_mails(imap)
101
        imap.check_mail do |msg|
102
          if @stop_asap
103
            logger.info "Bot was asked to stop..."
104
            break
105
          end
106

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

    
110
          # ignore mails not signed or encrypted
111
          unless mail.is_pgp_signed? or mail.is_pgp_encrypted?
112
            logger.info "Mail not signed/encrypted or not RFC3156 compliant, ignoring..."
113
            msg.delete
114
            next
115
          end
116

    
117
          logger.debug "RFC3156 content detected"
118
          begin
119
            report = mail.process
120
          rescue CyberError => e
121
            case e.severity
122
            when :grave
123
              logger.fatal "Fatal processing error, exiting (#{e.message})"
124
              exit 2
125
            when :unrecoverable
126
              logger.error "Internal processing error, skipping mail (#{e.message})"
127
              next
128
            when :processable
129
              logger.error "Untreated processing problem, skipping mail (#{e.message})"
130
              next
131
            when :ignorable
132
              logger.warn "Internal processing warning, continuing (#{e.message})"
133
            end
134
          end
135
          result_tag = report.ok? ? "SUCCESS" : "FAILURE"
136
          result_msg = "Processing result: #{result_tag}"
137
	  result_msg += " (#{report.error.untranslated})" unless report.ok?
138
          logger.info result_msg
139

    
140
          i18n = I18nController.instance
141
          i18n.set_language_for_user(report.user)
142

    
143
          unless report.ok?
144
            if report.warn_sender
145
              logger.info "Sending reply for rejected message"
146
              reply_intro = report.user ? _("Hello %{cn},", :cn =>report.user.cn) : _("Hello,")
147
              mail_reply = mail.create_simple_reject_reply(reply_intro.to_s + "\n\n" +
148
                           _("A message (ID: %{id}), apparently from you, was rejected for the following reason:",
149
                           :id => mail.message_id).to_s + "\n  " + report.error.to_s + "\n" + mail_signature())
150
              mail_reply.deliver
151
            end
152
            msg.delete
153
            next
154
          end
155

    
156
          order = MailOrder.parse(report.user, report.message)
157
          result_tag = order.valid? ? "SUCCESS" : "FAILURE"
158
          result_msg = "Processing result: #{result_tag}"
159
	  result_msg += " (#{order.error.untranslated})" unless order.valid?
160
          logger.info result_msg
161

    
162
          reply_intro = _("Hello %{cn},", :cn => order.user.cn)
163

    
164
          unless order.valid?
165
            logger.info "Sending reply for rejected order"
166
            mail_reply = mail.create_simple_reject_reply(reply_intro.to_s + "\n\n" +
167
                         _("An order, in a message (ID: %{id}) from you, was rejected for the following reason:",
168
                         :id => mail.message_id).to_s + "\n  " + order.error.to_s + "\n" + mail_signature())
169
            mail_reply.sign_and_crypt(order.user.keyFingerPrint)
170
            mail_reply.deliver
171
            msg.delete
172
            next
173
          end
174

    
175
          logger.debug "Message accepted, processing orders..."
176
          result_list = CommandRunner.run(order)
177

    
178
          # create transcript
179
          logger.debug "Preparing reply"
180
          reply_txt = reply_intro.to_s + "\n\n"
181
          reply_txt += _("Follows the transcript of your commands:").to_s + "\n"
182
          reply_attachments = []
183
          result_list.each do |result|
184
            reply_txt += "> #{result.cmd}\n"
185
            reply_txt += "#{result.message}\n"
186
            reply_attachments += result.refs unless result.refs.nil?
187
          end
188
          reply_txt += "\n" + mail_signature()
189

    
190
          # create mail
191
          logger.debug "Preparing mail"
192
          mail_reply = mail.create_reply
193
          if reply_attachments.empty?
194
            transcript_part = mail_reply
195
          else
196
            mail_reply.set_content_type("multipart", "mixed", {'boundary' => TMail.new_boundary})
197
            parts = []
198

    
199
            p = CyborgHood::Mail.new
200
            transcript_part = p
201
            mail_reply.parts << p
202

    
203
            reply_attachments.each do |attachment|
204
              p = CyborgHood::Mail.new
205
              p.set_content_type("text", "plain", {'charset' => "utf-8"})
206
              p.set_disposition("attachment", {'filename' => attachment.filename})
207
              p.quoted_printable_body = attachment.content
208
              mail_reply.parts << p
209
            end
210
          end
211
          # insert transcript
212
          transcript_part.set_content_type("text", "plain", {'charset' => 'utf-8', 'format' => 'flowed'})
213
          transcript_part.set_disposition("inline")
214
          transcript_part.quoted_printable_body = reply_txt
215

    
216
          # send reply
217
          logger.debug "Sending mail"
218
          mail_reply.sign_and_crypt(order.user.keyFingerPrint)
219
          mail_reply.deliver
220

    
221
          logger.info "Message processed completely, deleting"
222
          msg.delete
223
        end
224
      end
225

    
226
      def mail_signature
227
        s = "\n" +
228
          "-- \n" +
229
          "#{CyborgHood::PRODUCT} v#{CyborgHood::VERSION}\n"
230
        s += _("Contact eMail:").to_s + " \"#{@config.contact.name}\" <#{@config.contact.email}>\n" if @config.contact.email
231
        s += _("Contact URL:").to_s + " #{@config.contact.url}\n" if @config.contact.url
232
        s
233
      end
234
    end
235

    
236
    class CommandRunner
237
      include I18nTranslation
238

    
239
      def self.run(order)
240
        result_list = []
241
        order.commands.each do |cmd|
242
          result = OpenStruct.new
243
          result.cmd = cmd.cmdline
244
          result.ok = false
245

    
246
          if cmd.valid?
247
            logger.info "Executing command: #{cmd.cmdline}"
248
            begin
249
              execute_cmd(order.user, cmd.cmdsplit, order.shared_parameters, result)
250
            rescue CyberError => e
251
              result.message = e.message.capitalize + "."
252
            rescue
253
              logger.error "Command crashed: " + $!
254
	      logger.error "Crash trace: " + $!.backtrace.join("\n")
255
              result.message = _("Internal error. Administrator is warned.")
256
            end
257

    
258
            tag = result.ok ? "SUCCESS" : "FAILURE"
259
            logger.debug "Command result: [#{tag}] #{result.message.untranslated}"
260
          else
261
            logger.info "Invalid command detected: #{cmd.cmdline}"
262
	    cmd.parsing_errors.collect{|err| logger.debug "Invalid command detected - reason: " + err.untranslated }
263
            result.message = cmd.parsing_errors.collect{|err| err.to_s }.join("\n")
264
          end
265
          result_list << result
266
        end
267
        result_list
268
      end
269

    
270
      private
271

    
272
      def self.execute_cmd(user, cmdline, shared_parameters, result)
273
        subsys = cmdline.shift
274
        case subsys.upcase
275
        when "DNS"
276
          return if cmdline.empty?
277
          case cmdline.shift.upcase
278
          when "INFO"
279
            return unless cmdline.empty?
280
            list = CyborgHood::DnsDomain.find_by_manager(user)
281
            txt_list = list.collect{|z| z.cn }.sort.join(", ")
282
            result.ok = true
283
            result.message = _("You are manager of the following zones: %{zone_list}.", :zone_list => txt_list)
284
          when "GET"
285
            return if cmdline.empty?
286
            case cmdline.shift.upcase
287
            when "ZONE"
288
              return if cmdline.empty?
289
              zone = cmdline.shift.downcase
290

    
291
              dom = CyborgHood::DnsDomain.new(zone)
292
              unless dom.hosted?
293
                result.message = _("This zone is not hosted here.")
294
                return result
295
              end
296
              unless dom.managed_by? user
297
                result.message = _("You are not allowed to manage this zone.")
298
                return result
299
              end
300

    
301
              srv_dns = CyborgHood::Services::DNS.new(zone)
302
              result.ok = true
303
              result.message = _("Requested zone content attached.")
304
              zone_ref = {:content => srv_dns.read_zone, :filename => "dnszone_#{zone}.txt"}.to_ostruct
305
              result.refs = [zone_ref]
306
            end
307
          when "SET"
308
            return if cmdline.empty?
309
            case cmdline.shift.upcase
310
            when "ZONE"
311
            return if cmdline.empty?
312
              zone = cmdline.shift.downcase
313
              dom = CyborgHood::DnsDomain.new(zone)
314
              unless dom.hosted?
315
                result.message = _("This zone is not hosted here.")
316
                return result
317
              end
318
              unless dom.managed_by? user
319
                result.message = _("You are not allowed to manage this zone.")
320
                return result
321
              end
322
              srv_dns = CyborgHood::Services::DNS.new(zone)
323

    
324
              return if cmdline.empty?
325
              content_ref = cmdline.shift
326
              part = shared_parameters[content_ref]
327
              unless part.type == "text/plain"
328
                result.message = _("Attachment has wrong content-type.")
329
                return result
330
              end
331

    
332
              f = Tempfile.new(zone)
333
              f.write(part.content)
334
              f.close
335
              logger.debug "Created temporary zone file '#{f.path}'"
336

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

    
341
              dns_result = srv_dns.check_zone_file(f.path)
342
              unless dns_result.ok
343
                result.message = _("Invalid zone data.")
344
                f.close!
345
                return result
346
              end
347
              logger.debug "New serial: #{dns_result.serial}"
348
              # allow new serial or missing serial (to allow creating a new zone or replacing a broken zone)
349
              unless current_serial.nil? or dns_result.serial > current_serial
350
                result.message = _("Zone serial is not superior to current serial.")
351
                f.close!
352
                return result
353
              end
354

    
355
              begin
356
                srv_dns.write_zone_from_file(f.path)
357
                logger.debug "zone changed"
358
                if srv_dns.reload_zone
359
                  logger.debug "zone reloaded"
360
                  result.ok = true
361
                  result.message = _("Zone updated.")
362
                else
363
                  logger.warn "zone reload failed, replacing old content"
364
                  srv_dns.replace_zone_with_backup
365
                  result.message = _("Internal error. Administrator is warned.")
366
                end
367
              rescue
368
                logger.warn "Writing zone file failed"
369
                raise
370
              ensure
371
                f.close!
372
              end
373
            end
374
          end
375
        end
376

    
377
        if result.message.nil?
378
          # here fall lost souls
379
          result.message = _("Command not recognized.")
380
        end
381
      end
382
    end
383
  end
384
end
385

    
386
bot = CyborgHood::PostmanHome::Postman.new
387

    
388
trap('INT') do
389
  bot.ask_to_stop
390
end
391
trap('TERM') do
392
  bot.ask_to_stop
393
end
394

    
395
bot.run
(2-2/3)