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
|