require 'net/irc' require 'timeout' class CtcpChecker < Net::IRC::Client CTCP_SPAM_DELAY = 5 # `spam_target`: the nickname of the IRC user to CTCP spam. # `ctcp_cmds`: the Array of CTCP commands to send. # `expected_ctcp_replies`: Hash where the keys are the exact set of replies # we expect, and their values a regex the reply data must match. def initialize(host, port, spam_target, ctcp_cmds, expected_ctcp_replies) @spam_target = spam_target @ctcp_cmds = ctcp_cmds @expected_ctcp_replies = expected_ctcp_replies nickname = self.class.random_irc_nickname opts = { :nick => nickname, :user => nickname, :real => nickname, } opts[:logger] = Logger.new(DEBUG_LOG_PSEUDO_FIFO) super(host, port, opts) end # Makes sure that only the expected CTCP replies are received. def verify_ctcp_responses @sent_ctcp_cmds = Set.new @received_ctcp_replies = Set.new # Give 60 seconds for connecting to the server and other overhead # beyond the expected time to spam all CTCP commands. expected_ctcp_spam_time = @ctcp_cmds.length * CTCP_SPAM_DELAY timeout = expected_ctcp_spam_time + 60 begin Timeout::timeout(timeout) do start end rescue Timeout::Error # Do nothing as we'll check for errors below. ensure finish end ctcp_cmds_not_sent = @ctcp_cmds - @sent_ctcp_cmds.to_a expected_ctcp_replies_not_received = @expected_ctcp_replies.keys - @received_ctcp_replies.to_a if !ctcp_cmds_not_sent.empty? || !expected_ctcp_replies_not_received.empty? raise "Failed to spam all CTCP commands and receive the expected " + "replies within #{timeout} seconds.\n" + (ctcp_cmds_not_sent.empty? ? "" : "CTCP commands not sent: #{ctcp_cmds_not_sent}\n") + (expected_ctcp_replies_not_received.empty? ? "" : "Expected CTCP replies not received: " + expected_ctcp_replies_not_received.to_s) end end # Generate a random IRC nickname, in this case an alpha-numeric # string with length 10 to 15. To make it legal, the first character # is forced to be alpha. def self.random_irc_nickname random_alpha_string(1) + random_alnum_string(9, 14) end def spam(spam_target) post(NOTICE, spam_target, "Hi! I'm gonna test your CTCP capabilities now.") @ctcp_cmds.each do |cmd| sleep CTCP_SPAM_DELAY full_cmd = cmd case cmd when "PING" full_cmd += " #{Time.now.to_i}" when "ACTION" full_cmd += " barfs on the floor." when "ERRMSG" full_cmd += " Pidgin should not respond to this." end post(PRIVMSG, spam_target, ctcp_encode(full_cmd)) @sent_ctcp_cmds << cmd end end def on_rpl_welcome(m) super Thread.new { spam(@spam_target) } end def on_message(m) if m.command == ERR_NICKNAMEINUSE finish new_nick = self.class.random_irc_nickname @opts.marshal_load({ :nick => new_nick, :user => new_nick, :real => new_nick, }) start return end if m.ctcp? and /^:#{Regexp.escape(@spam_target)}!/.match(m) m.ctcps.each do |ctcp_reply| reply_type, _, reply_data = ctcp_reply.partition(" ") if @expected_ctcp_replies.has_key?(reply_type) if @expected_ctcp_replies[reply_type].match(reply_data) @received_ctcp_replies << reply_type else raise "Received expected CTCP reply '#{reply_type}' but with " + "unexpected data '#{reply_data}' " end else raise "Received unexpected CTCP reply '#{reply_type}' with " + "data '#{reply_data}'" end end end if Set.new(@ctcp_cmds) == @sent_ctcp_cmds && \ Set.new(@expected_ctcp_replies.keys) == @received_ctcp_replies finish end end end