+#-- vim:sw=2:et
+#++
+#
+# :title: IRC Socket
+#
+# This module implements the IRC socket interface, including IRC message
+# penalty computation and the message queue system
+
require 'monitor'
class ::String
# Calculate the penalty which will be assigned to this message
# by the IRCd
def irc_send_penalty
- # According to eggrdop, the initial penalty is
+ # According to eggdrop, the initial penalty is
penalty = 1 + self.size/100
# on everything but UnderNET where it's
# penalty = 2 + self.size/120
dests = pars.split($;,2).first
penalty += dests.split(',').size
when :WHO
- # I'm too lazy to implement this one correctly
- penalty += 5
+ args = pars.split
+ if args.length > 0
+ penalty += args.inject(0){ |sum,x| sum += ((x.length > 4) ? 3 : 5) }
+ else
+ penalty += 10
+ end
+ when :PART
+ penalty += 4
when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS
penalty += 2
when :INVITE, :NICK
# accumulator for the throttle
attr_reader :throttle_bytes
- # delay between lines sent
- attr_accessor :sendq_delay
-
- # max lines to burst
- attr_accessor :sendq_burst
-
# an optional filter object. we call @filter.in(data) for
# all incoming data and @filter.out(data) for all outgoing data
attr_reader :filter
# normalized uri of the current server
attr_reader :server_uri
+ # penalty multiplier (percent)
+ attr_accessor :penalty_pct
+
# default trivial filter class
class IdentityFilter
def in(x)
# server_list:: list of servers to connect to
# host:: optional local host to bind to (ruby 1.7+ required)
# create a new Irc::Socket
- def initialize(server_list, host, sendq_delay=2, sendq_burst=4, opts={})
+ def initialize(server_list, host, opts={})
@server_list = server_list.dup
@server_uri = nil
@conn_count = 0
@spooler = false
@lines_sent = 0
@lines_received = 0
- if opts.kind_of?(Hash) and opts.key?(:ssl)
- @ssl = opts[:ssl]
- else
- @ssl = false
- end
-
- if sendq_delay
- @sendq_delay = sendq_delay.to_f
- else
- @sendq_delay = 2
- end
- if sendq_burst
- @sendq_burst = sendq_burst.to_i
- else
- @sendq_burst = 4
- end
+ @ssl = opts[:ssl]
+ @ssl_verify = opts[:ssl_verify]
+ @ssl_ca_file = opts[:ssl_ca_file]
+ @ssl_ca_path = opts[:ssl_ca_path]
+ @penalty_pct = opts[:penalty_pct] || 100
end
def connected?
@conn_count += 1
@server_uri = URI.parse(srv_uri)
@server_uri.port = 6667 if !@server_uri.port
+
debug "connection attempt \##{@conn_count} (#{@server_uri.host}:#{@server_uri.port})"
+ # if the host is a bracketed (IPv6) address, strip the brackets
+ # since Ruby doesn't like them in the Socket host parameter
+ # FIXME it would be safer to have it check for a valid
+ # IPv6 bracketed address rather than just stripping the brackets
+ srv_host = @server_uri.host
+ if srv_host.match(/\A\[(.*)\]\z/)
+ srv_host = $1
+ end
+
if(@host)
begin
- @sock=TCPSocket.new(@server_uri.host, @server_uri.port, @host)
+ sock=TCPSocket.new(srv_host, @server_uri.port, @host)
rescue ArgumentError => e
error "Your version of ruby does not support binding to a "
error "specific local address, please upgrade if you wish "
error "to use HOST = foo"
error "(this option has been disabled in order to continue)"
- @sock=TCPSocket.new(@server_uri.host, @server_uri.port)
+ sock=TCPSocket.new(srv_host, @server_uri.port)
end
else
- @sock=TCPSocket.new(@server_uri.host, @server_uri.port)
+ sock=TCPSocket.new(srv_host, @server_uri.port)
end
if(@ssl)
require 'openssl'
ssl_context = OpenSSL::SSL::SSLContext.new()
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
- @rawsock = @sock
- @sock = OpenSSL::SSL::SSLSocket.new(@rawsock, ssl_context)
- @sock.sync_close = true
- @sock.connect
+ if @ssl_verify
+ ssl_context.ca_file = @ssl_ca_file if @ssl_ca_file and not @ssl_ca_file.empty?
+ ssl_context.ca_path = @ssl_ca_path if @ssl_ca_path and not @ssl_ca_path.empty?
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ else
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context)
+ sock.sync_close = true
+ sock.connect
end
- @last_send = Time.new - @sendq_delay
+ @sock = sock
+ @last_send = Time.new
@flood_send = Time.new
- @last_throttle = Time.new
@burst = 0
@sock.extend(MonitorMixin)
@sendq = MessageQueue.new
rescue Exception => e
error "error while shutting down: #{e.pretty_inspect}"
end
- @rawsock = nil if @ssl
@sock = nil
- @burst = 0
+ @server_uri = nil
@sendq.clear
end
def writer_loop
loop do
- # we could wait for the message, then calculate the delay and sleep
- # if necessary. however, if high-priority message is enqueued while
- # we sleep, it won't be the first to go out when the sleep is over.
- # thus, we have to call Time.now() twice, once to calculate the delay
- # and once to adjust @burst / @flood_send.
begin
now = Time.now
- if @sendq_delay > 0
- burst_delay = 0
- if @burst > @sendq_burst
- burst_delay = @last_send + @sendq_delay - now
- end
-
- flood_delay = @flood_send - MAX_IRC_SEND_PENALTY - now
- delay = [burst_delay, flood_delay, 0].max
- if delay > 0
- debug "sleep(#{delay}) # (f: #{flood_delay}, b: #{burst_delay})"
- sleep(delay)
- end
+ flood_delay = @flood_send - MAX_IRC_SEND_PENALTY - now
+ delay = [flood_delay, 0].max
+ if delay > 0
+ debug "sleep(#{delay}) # (f: #{flood_delay})"
+ sleep(delay)
end
msg = @sendq.shift
- now = Time.now
- @flood_send = now if @flood_send < now
- @burst = 0 if @last_send + @sendq_delay < now
debug "got #{msg.inspect} from queue, sending"
emergency_puts(msg, true)
rescue Exception => e
if @sock.nil?
error "SEND attempted on closed socket"
else
- @sock.puts(@filter.out(message))
- @last_send = Time.new
- @flood_send += message.irc_send_penalty if penalty
+ # we use Socket#syswrite() instead of Socket#puts() because
+ # the latter is racy and can cause double message output in
+ # some circumstances
+ actual = @filter.out(message) + "\n"
+ now = Time.new
+ @sock.syswrite actual
+ @last_send = now
+ @flood_send = now if @flood_send < now
+ @flood_send += message.irc_send_penalty*@penalty_pct/100.0 if penalty
@lines_sent += 1
- @burst += 1
end
rescue Exception => e
handle_socket_error(:SEND, e)