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(" ")}") + "|"
47 STDERR.puts "-> #{data}" if @debug
51 def call_callback(callback, *args)
52 @callbacks[callback].each do |proc|
54 proc.call(self, *args)
56 STDERR.puts "Exception: #{e.message}\n#{e.backtrae}"
61 def connection_completed
62 call_callback :connected
65 def receive_line(line)
67 STDERR.puts "<- #{line}" if @debug
68 line = unsanitize(line)
69 cmd = line.slice!(/^\S+/)
72 if cmd =~ /^<.*>$/ then
73 # this must be a public message
75 call_callback :message, nick, line, false, false
76 elsif cmd =~ /^\$\S+$/ then
77 # this is a proper command
79 # hardcode the $To: command since the colon is ugly
80 # this protocol is pretty messy
81 cmd = "To" if cmd == "To:"
82 if self.respond_to? "cmd_#{cmd}" then
83 self.send "cmd_#{cmd}", line
85 call_callback :error, "Unknown command: $#{cmd} #{line}"
88 call_callback :error, "Garbage data: #{line}"
93 @callbacks = Hash.new { |h,k| h[k] = [] }
103 class DCClientProtocol < DCProtocol
104 # known keys for args are:
105 # password - server password
106 # debug - should this socket print debug data?
111 # version - version number for the tag
112 # slots - number of slots to declare as open
113 def self.connect(host, port, nickname, args = {})
114 EventMachine::connect(host, port, self) do |c|
118 @debug = args[:debug]
119 @config[:description] ||= ""
120 @config[:speed] ||= "Bot"
121 @config[:speed_class] ||= 1
122 @config[:email] ||= ""
123 @config[:version] ||= "0.1"
124 @config[:slots] ||= 0
126 yield c if block_given?
130 def sendPublicMessage(message)
131 data = sanitize("<#{@nickname}> #{message}") + "|"
135 def sendPrivateMessage(recipient, message)
136 send_command "To:", recipient, "From:", @nickname, "$<#{@nickname}>", message
144 attr_reader :nickname, :hubname, :quit
146 # protocol implementation
149 lock = line.split(" ")[0]
150 key = lockToKey(lock)
152 send_command("Key", "#{key}")
153 send_command("ValidateNick", "#{@nickname}")
156 def cmd_ValidateDenide(line)
157 call_callback :error, "Nickname in use or invalid"
161 def cmd_GetPass(line)
162 if @config.has_key? :password
163 send_command "MyPass", @config[:password]
165 call_callback :error, "Password required but not given"
170 def cmd_BadPass(line)
171 call_callback :error, "Bad password given"
175 def cmd_LogedIn(line)
176 call_callback :logged_in
179 def cmd_HubName(line)
181 call_callback :hubname, @hubname
186 if nick == @nickname then
187 # this is us, we should respond
188 send_command "Version", "1,0091"
189 send_command "GetNickList"
190 send_command "MyINFO", "$ALL #{@nickname} #{@config[:description]}<RubyBot V:#{@config[:version]},M:P,H:1/0/0,S:#{@config[:slots]}>$", \
191 "$#{@config[:speed]}#{@config[:speed_class].chr}$#{@config[:email]}$0$"
193 call_callback :user_connected, nick
197 def cmd_NickList(line)
198 call_callback :nicklist, line.split("$$")
202 call_callback :oplist, line.split("$$")
206 if line =~ /^\$ALL (\S+) ([^$]*)\$ +\$([^$]*)\$([^$]*)\$([^$]*)\$$/ then
212 if speed.length > 0 and speed[-1] < 0x20 then
213 # assume last byte a control character means it's the speed class
214 speed_class = speed.slice!(-1)
218 call_callback :info, nick, interest, speed, speed_class, email, sharesize unless nick == @nickname
222 def cmd_ConnectToMe(line)
223 # another peer is trying to connect to me
224 if line =~ /^(\S+) (\S+):(\d+)$/ then
228 if mynick == @nickname then
229 connect_to_peer(ip, port)
231 call_callback :error, "Strange ConnectToMe request: #{line}"
236 def cmd_RevConnectToMe(line)
237 if line =~ /^(\S+) (\S+)$/ then
238 # for the moment we're just going to be a passive client
241 if mynick == @nickname then
242 call_callback :reverse_connection, nick
243 send_command "RevConnectToMe", mynick, nick
245 call_callback :error, "Strange RevConnectToMe request: #{line}"
251 if line =~ /^(\S+) From: (\S+) \$<(\S+)> (.*)$/ then
254 displaynick = $3 # ignored for now
256 call_callback :message, nick, message, true, (displaynick == "*")
258 call_callback :error, "Garbage $To: #{line}"
264 call_callback :user_quit, nick
268 # for the moment, completely ignore this
273 def connect_to_peer(ip, port)
274 @peers << EventMachine::connect(ip, port, DCPeerProtocol) do |c|
281 c.call_callback :initialized
285 # event handling methods
291 self.registerCallback :peer_unbind do |socket, peer|
298 @peers.each do |peer|
299 peer.close_connection
305 # major assumption in this implementation is that we are simply uploading
306 # if we want to be able to initiate downloads, this needs some tweaking
307 # we're also a passive client, so we're always connecting to the other client
308 class DCPeerProtocol < DCProtocol
309 XML_FILE_LISTING = <<EOF
310 <?xml version="1.0" encoding="utf-8"?>
311 <FileListing Version="1" Generator="RubyBot">
312 <Directory Name="Send a /pm with !help for help">
316 DCLST_FILE_LISTING = <<EOF
317 Send a /pm with !help for help
319 DCLST_FILE_LISTING_HE3 = he3_encode(DCLST_FILE_LISTING)
321 attr_reader :remote_nick, :host, :port
327 # callbacks triggered from the peer always begin with peer_
328 def call_callback(name, *args)
329 @parent.call_callback "peer_#{name.to_s}".to_sym, self, *args
332 def connection_completed
334 send_command "MyNick", @parent.nickname
335 send_command "Lock", "FOO", "Pk=BAR"
343 lock = line.split(" ")[0]
344 key = lockToKey(lock)
345 send_command "Direction", "Upload", rand(0x7FFF)
346 send_command "Key", key
350 # who cares if they got the key right? just ignore it
353 def cmd_Direction(line)
354 direction, rnd = line.split(" ")
355 if direction != "Download" then
356 # why did they send me a ConnectToMe if they don't want to download?
357 call_callback :error, "Unexpected peer direction: #{direction}"
362 def cmd_GetListLen(line)
363 send_command "ListLen", DCLST_FILE_LISTING_HE3.length
367 if line =~ /^([^$]+)\$(\d+)$/ then
369 offset = $2.to_i - 1 # it's 1-based
370 call_callback :get, @filename
371 if @filename == "MyList.DcLst" then
372 @fileio = StringIO.new(DCLST_FILE_LISTING_HE3)
374 send_command "FileLength", @fileio.size - @fileio.pos
376 send_command "Error", "File Not Available"
377 close_connection_after_writing
380 send_command "Error", "Unknown $Get format"
381 close_connection_after_writing
387 # we haven't been asked for the file yet
388 send_command "Error", "Unexpected $Send"
389 close_connection_after_writing
391 data = @fileio.read(40906)
396 def cmd_Canceled(line)
401 call_callback :error, "Peer Error: #{line}"