2 require 'cgi' # for entity-escaping
5 class DCProtocol < EventMachine::Connection
6 include EventMachine::Protocols::LineText2
8 def registerCallback(callback, &block)
9 @callbacks[callback] = block
13 key = String.new(lock)
14 1.upto(key.size - 1) do |i|
15 key[i] = lock[i] ^ lock[i-1]
17 key[0] = lock[0] ^ lock[-1] ^ lock[-2] ^ 5
20 0.upto(key.size - 1) do |i|
21 key[i] = ((key[i]<<4) & 240) | ((key[i]>>4) & 15)
24 0.upto(key.size - 1) do |i|
25 if [0,5,36,96,124,126].include?(key[i]) then
26 key[i,1] = ("/%%DCN%03d%%/" % key[i])
34 data.gsub("|", "|")
38 CGI.unescapeHTML(data)
41 def send_command(cmd, *args)
42 data = sanitize("$#{cmd}#{["", *args].join(" ")}") + "|"
43 STDERR.puts "-> #{data}" if $debug
47 def call_callback(callback, *args)
48 @callbacks[callback].call(self, *args) if @callbacks.has_key? callback
51 def connection_completed
52 call_callback :connected
55 def receive_line(line)
57 STDERR.puts "<- #{line}" if $debug
58 line = unsanitize(line)
59 cmd = line.slice!(/^\S+/)
62 if cmd =~ /^<.*>$/ then
63 # this must be a public message
65 call_callback :message, nick, line, false, false
66 elsif cmd =~ /^\$\S+$/ then
67 # this is a proper command
69 # hardcode the $To: command since the colon is ugly
70 # this protocol is pretty messy
71 cmd = "To" if cmd == "To:"
72 if self.respond_to? "cmd_#{cmd}" then
73 self.send "cmd_#{cmd}", line
75 STDERR.puts "! Unknown command: $#{cmd} #{line}"
78 STDERR.puts "! Garbage data: #{line}"
92 class DCClientProtocol < DCProtocol
95 # known keys for args are:
101 def self.connect(host, port, nickname, args = {})
102 EventMachine::connect(host, port, self) do |c|
106 @config[:description] ||= ""
107 @config[:speed] ||= "Bot"
108 @config[:speed_class] ||= 1
111 yield c if block_given?
115 def sendPublicMessage(message)
116 data = sanitize("<#{@nickname}> #{message}") + "|"
117 STDERR.puts "-> #{data}" if $debug
121 def sendPrivateMessage(recipient, message)
122 send_command "To:", recipient, "From:", @nickname, "$<#{@nickname}>", message
130 attr_reader :nickname, :hubname, :quit
132 # protocol implementation
135 lock = line.split(" ")[0]
136 key = lockToKey(lock)
138 send_command("Key", "#{key}")
139 send_command("ValidateNick", "#{@nickname}")
142 def cmd_ValidateDenide(line)
143 STDERR.puts "Nickname in use or invalid"
147 def cmd_GetPass(line)
148 if @config.has_key? :password
149 send_command "MyPass", @config[:password]
151 STDERR.puts "Password required but not given"
156 def cmd_BadPass(line)
157 STDERR.puts "Bad password given"
161 def cmd_LogedIn(line)
162 call_callback :logged_in
165 def cmd_HubName(line)
167 call_callback :hubname, @hubname
172 if nick == @nickname then
173 # this is us, we should respond
174 send_command "Version", "1,0091"
175 send_command "GetNickList"
176 send_command "MyINFO", "$ALL #{@nickname} #{@config[:description]}<RubyBot V:#{RUBYBOT_VERSION}>$", \
177 "$#{@config[:speed]}#{@config[:speed_class].chr}$#{@config[:email]}$0$"
179 call_callback :user_connected, nick
183 def cmd_NickList(line)
184 call_callback :nicklist, line.split("$$")
188 call_callback :oplist, line.split("$$")
192 if line =~ /^\$ALL (\S+) ([^$]*)\$ +\$([^$]*)\$([^$]*)\$([^$]*)\$$/ then
198 if speed.length > 0 and speed[-1] < 0x20 then
199 # assume last byte a control character means it's the speed class
200 speed_class = speed.slice!(-1)
204 call_callback :info, nick, interest, speed, speed_class, email, sharesize unless nick == @nickname
208 def cmd_ConnectToMe(line)
209 # another peer is trying to connect to me
210 if line =~ /^(\S+) (\S+):(\d+)$/ then
214 if mynick == @nickname then
215 connect_to_peer(ip, port)
217 STDERR.puts "! Strange ConnectToMe request: #{line}"
222 def cmd_RevConnectToMe(line)
223 if line =~ /^(\S+) (\S+)$/ then
224 # for the moment we're just going to be a passive client
227 if mynick == @nickname then
228 STDERR.puts "* Bouncing RevConnectToMe back to user: #{nick}"
229 send_command "RevConnectToMe", mynick, nick
231 STDERR.puts "! Strange RevConnectToMe request: #{line}"
237 if line =~ /^(\S+) From: (\S+) \$<(\S+)> (.*)$/ then
240 displaynick = $3 # ignored for now
242 call_callback :message, nick, message, true, (displaynick == "*")
244 STDERR.puts "Garbage To: #{line}"
250 call_callback :user_quit, nick
254 # for the moment, completely ignore this
259 def connect_to_peer(ip, port)
260 STDERR.puts "* Connecting to peer: #{ip}:#{port}"
261 @peers << EventMachine::connect(ip, port, DCPeerProtocol) do |c|
263 c.registerCallback :unbind do |socket|
264 STDERR.puts "* Connection to peer closed"
270 # event handling methods
279 # major assumption in this implementation is that we are simply uploading
280 # if we want to be able to initiate downloads, this needs some tweaking
281 # we're also a passive client, so we're always connecting to the other client
282 class DCPeerProtocol < DCProtocol
283 XML_FILE_LISTING = <<EOF
284 <?xml version="1.0" encoding="utf-8"?>
285 <FileListing Version="1" Generator="RubyBot">
286 <Directory Name="Send a /pm with !help for help">
290 DCLST_FILE_LISTING = <<EOF
291 Send a /pm with !help for help
293 DCLST_FILE_LISTING_HE3 = he3_encode(DCLST_FILE_LISTING)
301 def connection_completed
303 send_command "MyNick", @parent.nickname
304 send_command "Lock", "FOO", "Pk=BAR"
312 lock = line.split(" ")[0]
313 key = lockToKey(lock)
314 send_command "Direction", "Upload", rand(0x7FFF)
315 send_command "Key", key
319 # who cares if they got the key right? just ignore it
322 def cmd_Direction(line)
323 direction, rnd = line.split(" ")
324 if direction != "Download" then
325 # why did they send me a ConnectToMe if they don't want to download?
326 STDERR.puts "! Unexpected peer direction: #{direction}"
331 def cmd_GetListLen(line)
332 send_command "ListLen", DCLST_FILE_LISTING_HE3.length
336 if line =~ /^([^$]+)\$(\d+)$/ then
338 @offset = $2.to_i - 1 # it's 1-based
339 @fileio = StringIO.new(DCLST_FILE_LISTING_HE3)
340 @fileio.pos = @offset
341 if @filename == "MyList.DcLst" then
342 send_command "FileLength", @fileio.size - @fileio.pos
344 send_command "Error", "File Not Available"
347 send_command "Error", "Unknown $Get format"
348 close_connection_after_writing
353 if @filename.nil? then
354 # we haven't been asked for the file yet
355 send_command "Error", "Unexpected $Send"
356 close_connection_after_writing
358 data = @fileio.read(40906)
359 STDERR.puts "-> #{data}" if $debug
364 def cmd_Canceled(line)
369 STDERR.puts "! Peer Error: #{line}"