PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/rbot/ircsocket.rb

http://github.com/jsn/rbot
Ruby | 476 lines | 358 code | 56 blank | 62 comment | 38 complexity | 842043539de1726a8ee167cbb284f8e3 MD5 | raw file
  1. #-- vim:sw=2:et
  2. #++
  3. #
  4. # :title: IRC Socket
  5. #
  6. # This module implements the IRC socket interface, including IRC message
  7. # penalty computation and the message queue system
  8. require 'monitor'
  9. class ::String
  10. # Calculate the penalty which will be assigned to this message
  11. # by the IRCd
  12. def irc_send_penalty
  13. # According to eggdrop, the initial penalty is
  14. penalty = 1 + self.size/100
  15. # on everything but UnderNET where it's
  16. # penalty = 2 + self.size/120
  17. cmd, pars = self.split($;,2)
  18. debug "cmd: #{cmd}, pars: #{pars.inspect}"
  19. case cmd.to_sym
  20. when :KICK
  21. chan, nick, msg = pars.split
  22. chan = chan.split(',')
  23. nick = nick.split(',')
  24. penalty += nick.size
  25. penalty *= chan.size
  26. when :MODE
  27. chan, modes, argument = pars.split
  28. extra = 0
  29. if modes
  30. extra = 1
  31. if argument
  32. extra += modes.split(/\+|-/).size
  33. else
  34. extra += 3 * modes.split(/\+|-/).size
  35. end
  36. end
  37. if argument
  38. extra += 2 * argument.split.size
  39. end
  40. penalty += extra * chan.split.size
  41. when :TOPIC
  42. penalty += 1
  43. penalty += 2 unless pars.split.size < 2
  44. when :PRIVMSG, :NOTICE
  45. dests = pars.split($;,2).first
  46. penalty += dests.split(',').size
  47. when :WHO
  48. args = pars.split
  49. if args.length > 0
  50. penalty += args.inject(0){ |sum,x| sum += ((x.length > 4) ? 3 : 5) }
  51. else
  52. penalty += 10
  53. end
  54. when :PART
  55. penalty += 4
  56. when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS
  57. penalty += 2
  58. when :INVITE, :NICK
  59. penalty += 3
  60. when :ISON
  61. penalty += 1
  62. else # Unknown messages
  63. penalty += 1
  64. end
  65. if penalty > 99
  66. debug "Wow, more than 99 secs of penalty!"
  67. penalty = 99
  68. end
  69. if penalty < 2
  70. debug "Wow, less than 2 secs of penalty!"
  71. penalty = 2
  72. end
  73. debug "penalty: #{penalty}"
  74. return penalty
  75. end
  76. end
  77. module Irc
  78. require 'socket'
  79. require 'thread'
  80. class QueueRing
  81. # A QueueRing is implemented as an array with elements in the form
  82. # [chan, [message1, message2, ...]
  83. # Note that the channel +chan+ has no actual bearing with the channels
  84. # to which messages will be sent
  85. def initialize
  86. @storage = Array.new
  87. @last_idx = -1
  88. end
  89. def clear
  90. @storage.clear
  91. @last_idx = -1
  92. end
  93. def length
  94. len = 0
  95. @storage.each {|c|
  96. len += c[1].size
  97. }
  98. return len
  99. end
  100. alias :size :length
  101. def empty?
  102. @storage.empty?
  103. end
  104. def push(mess, chan)
  105. cmess = @storage.assoc(chan)
  106. if cmess
  107. idx = @storage.index(cmess)
  108. cmess[1] << mess
  109. @storage[idx] = cmess
  110. else
  111. @storage << [chan, [mess]]
  112. end
  113. end
  114. def next
  115. if empty?
  116. warning "trying to access empty ring"
  117. return nil
  118. end
  119. save_idx = @last_idx
  120. @last_idx = (@last_idx + 1) % @storage.size
  121. mess = @storage[@last_idx][1].first
  122. @last_idx = save_idx
  123. return mess
  124. end
  125. def shift
  126. if empty?
  127. warning "trying to access empty ring"
  128. return nil
  129. end
  130. @last_idx = (@last_idx + 1) % @storage.size
  131. mess = @storage[@last_idx][1].shift
  132. @storage.delete(@storage[@last_idx]) if @storage[@last_idx][1] == []
  133. return mess
  134. end
  135. end
  136. class MessageQueue
  137. def initialize
  138. # a MessageQueue is an array of QueueRings
  139. # rings have decreasing priority, so messages in ring 0
  140. # are more important than messages in ring 1, and so on
  141. @rings = Array.new(3) { |i|
  142. if i > 0
  143. QueueRing.new
  144. else
  145. # ring 0 is special in that if it's not empty, it will
  146. # be popped. IOW, ring 0 can starve the other rings
  147. # ring 0 is strictly FIFO and is therefore implemented
  148. # as an array
  149. Array.new
  150. end
  151. }
  152. # the other rings are satisfied round-robin
  153. @last_ring = 0
  154. self.extend(MonitorMixin)
  155. @non_empty = self.new_cond
  156. end
  157. def clear
  158. self.synchronize do
  159. @rings.each { |r| r.clear }
  160. @last_ring = 0
  161. end
  162. end
  163. def push(mess, chan=nil, cring=0)
  164. ring = cring
  165. self.synchronize do
  166. if ring == 0
  167. warning "message #{mess} at ring 0 has channel #{chan}: channel will be ignored" if !chan.nil?
  168. @rings[0] << mess
  169. else
  170. error "message #{mess} at ring #{ring} must have a channel" if chan.nil?
  171. @rings[ring].push mess, chan
  172. end
  173. @non_empty.signal
  174. end
  175. end
  176. def shift(tmout = nil)
  177. self.synchronize do
  178. @non_empty.wait(tmout) if self.empty?
  179. return unsafe_shift
  180. end
  181. end
  182. protected
  183. def empty?
  184. !@rings.find { |r| !r.empty? }
  185. end
  186. def length
  187. @rings.inject(0) { |s, r| s + r.size }
  188. end
  189. alias :size :length
  190. def unsafe_shift
  191. if !@rings[0].empty?
  192. return @rings[0].shift
  193. end
  194. (@rings.size - 1).times do
  195. @last_ring = (@last_ring % (@rings.size - 1)) + 1
  196. return @rings[@last_ring].shift unless @rings[@last_ring].empty?
  197. end
  198. warning "trying to access an empty message queue"
  199. return nil
  200. end
  201. end
  202. # wrapped TCPSocket for communication with the server.
  203. # emulates a subset of TCPSocket functionality
  204. class Socket
  205. MAX_IRC_SEND_PENALTY = 10
  206. # total number of lines sent to the irc server
  207. attr_reader :lines_sent
  208. # total number of lines received from the irc server
  209. attr_reader :lines_received
  210. # total number of bytes sent to the irc server
  211. attr_reader :bytes_sent
  212. # total number of bytes received from the irc server
  213. attr_reader :bytes_received
  214. # accumulator for the throttle
  215. attr_reader :throttle_bytes
  216. # an optional filter object. we call @filter.in(data) for
  217. # all incoming data and @filter.out(data) for all outgoing data
  218. attr_reader :filter
  219. # normalized uri of the current server
  220. attr_reader :server_uri
  221. # penalty multiplier (percent)
  222. attr_accessor :penalty_pct
  223. # default trivial filter class
  224. class IdentityFilter
  225. def in(x)
  226. x
  227. end
  228. def out(x)
  229. x
  230. end
  231. end
  232. # set filter to identity, not to nil
  233. def filter=(f)
  234. @filter = f || IdentityFilter.new
  235. end
  236. # server_list:: list of servers to connect to
  237. # host:: optional local host to bind to (ruby 1.7+ required)
  238. # create a new Irc::Socket
  239. def initialize(server_list, host, opts={})
  240. @server_list = server_list.dup
  241. @server_uri = nil
  242. @conn_count = 0
  243. @host = host
  244. @sock = nil
  245. @filter = IdentityFilter.new
  246. @spooler = false
  247. @lines_sent = 0
  248. @lines_received = 0
  249. @ssl = opts[:ssl]
  250. @ssl_verify = opts[:ssl_verify]
  251. @ssl_ca_file = opts[:ssl_ca_file]
  252. @ssl_ca_path = opts[:ssl_ca_path]
  253. @penalty_pct = opts[:penalty_pct] || 100
  254. end
  255. def connected?
  256. !@sock.nil?
  257. end
  258. # open a TCP connection to the server
  259. def connect
  260. if connected?
  261. warning "reconnecting while connected"
  262. return
  263. end
  264. srv_uri = @server_list[@conn_count % @server_list.size].dup
  265. srv_uri = 'irc://' + srv_uri if !(srv_uri =~ /:\/\//)
  266. @conn_count += 1
  267. @server_uri = URI.parse(srv_uri)
  268. @server_uri.port = 6667 if !@server_uri.port
  269. debug "connection attempt \##{@conn_count} (#{@server_uri.host}:#{@server_uri.port})"
  270. # if the host is a bracketed (IPv6) address, strip the brackets
  271. # since Ruby doesn't like them in the Socket host parameter
  272. # FIXME it would be safer to have it check for a valid
  273. # IPv6 bracketed address rather than just stripping the brackets
  274. srv_host = @server_uri.host
  275. if srv_host.match(/\A\[(.*)\]\z/)
  276. srv_host = $1
  277. end
  278. if(@host)
  279. begin
  280. sock=TCPSocket.new(srv_host, @server_uri.port, @host)
  281. rescue ArgumentError => e
  282. error "Your version of ruby does not support binding to a "
  283. error "specific local address, please upgrade if you wish "
  284. error "to use HOST = foo"
  285. error "(this option has been disabled in order to continue)"
  286. sock=TCPSocket.new(srv_host, @server_uri.port)
  287. end
  288. else
  289. sock=TCPSocket.new(srv_host, @server_uri.port)
  290. end
  291. if(@ssl)
  292. require 'openssl'
  293. ssl_context = OpenSSL::SSL::SSLContext.new()
  294. if @ssl_verify
  295. ssl_context.ca_file = @ssl_ca_file if @ssl_ca_file and not @ssl_ca_file.empty?
  296. ssl_context.ca_path = @ssl_ca_path if @ssl_ca_path and not @ssl_ca_path.empty?
  297. ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
  298. else
  299. ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
  300. end
  301. sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context)
  302. sock.sync_close = true
  303. sock.connect
  304. end
  305. @sock = sock
  306. @last_send = Time.new
  307. @flood_send = Time.new
  308. @burst = 0
  309. @sock.extend(MonitorMixin)
  310. @sendq = MessageQueue.new
  311. @qthread = Thread.new { writer_loop }
  312. end
  313. # used to send lines to the remote IRCd by skipping the queue
  314. # message: IRC message to send
  315. # it should only be used for stuff that *must not* be queued,
  316. # i.e. the initial PASS, NICK and USER command
  317. # or the final QUIT message
  318. def emergency_puts(message, penalty = false)
  319. @sock.synchronize do
  320. # debug "In puts - got @sock"
  321. puts_critical(message, penalty)
  322. end
  323. end
  324. def handle_socket_error(string, e)
  325. error "#{string} failed: #{e.pretty_inspect}"
  326. # We assume that an error means that there are connection
  327. # problems and that we should reconnect, so we
  328. shutdown
  329. raise SocketError.new(e.inspect)
  330. end
  331. # get the next line from the server (blocks)
  332. def gets
  333. if @sock.nil?
  334. warning "socket get attempted while closed"
  335. return nil
  336. end
  337. begin
  338. reply = @filter.in(@sock.gets)
  339. @lines_received += 1
  340. reply.strip! if reply
  341. debug "RECV: #{reply.inspect}"
  342. return reply
  343. rescue Exception => e
  344. handle_socket_error(:RECV, e)
  345. end
  346. end
  347. def queue(msg, chan=nil, ring=0)
  348. @sendq.push msg, chan, ring
  349. end
  350. def clearq
  351. @sendq.clear
  352. end
  353. # flush the TCPSocket
  354. def flush
  355. @sock.flush
  356. end
  357. # Wraps Kernel.select on the socket
  358. def select(timeout=nil)
  359. Kernel.select([@sock], nil, nil, timeout)
  360. end
  361. # shutdown the connection to the server
  362. def shutdown(how=2)
  363. return unless connected?
  364. @qthread.kill
  365. @qthread = nil
  366. begin
  367. @sock.close
  368. rescue Exception => e
  369. error "error while shutting down: #{e.pretty_inspect}"
  370. end
  371. @sock = nil
  372. @server_uri = nil
  373. @sendq.clear
  374. end
  375. private
  376. def writer_loop
  377. loop do
  378. begin
  379. now = Time.now
  380. flood_delay = @flood_send - MAX_IRC_SEND_PENALTY - now
  381. delay = [flood_delay, 0].max
  382. if delay > 0
  383. debug "sleep(#{delay}) # (f: #{flood_delay})"
  384. sleep(delay)
  385. end
  386. msg = @sendq.shift
  387. debug "got #{msg.inspect} from queue, sending"
  388. emergency_puts(msg, true)
  389. rescue Exception => e
  390. error "Spooling failed: #{e.pretty_inspect}"
  391. debug e.backtrace.join("\n")
  392. raise e
  393. end
  394. end
  395. end
  396. # same as puts, but expects to be called with a lock held on @sock
  397. def puts_critical(message, penalty=false)
  398. # debug "in puts_critical"
  399. begin
  400. debug "SEND: #{message.inspect}"
  401. if @sock.nil?
  402. error "SEND attempted on closed socket"
  403. else
  404. # we use Socket#syswrite() instead of Socket#puts() because
  405. # the latter is racy and can cause double message output in
  406. # some circumstances
  407. actual = @filter.out(message) + "\n"
  408. now = Time.new
  409. @sock.syswrite actual
  410. @last_send = now
  411. @flood_send = now if @flood_send < now
  412. @flood_send += message.irc_send_penalty*@penalty_pct/100.0 if penalty
  413. @lines_sent += 1
  414. end
  415. rescue Exception => e
  416. handle_socket_error(:SEND, e)
  417. end
  418. end
  419. end
  420. end