summaryrefslogtreecommitdiffstats
path: root/features/support/helpers/ctcp_helper.rb
blob: ee5180ab84f1a627e69243b20cc3ca881ff8325b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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