Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add the server end: listeners, client handling.

Sort out the config a bit more.
Flesh out the start-up sequence.
Add listeners for the server end.
Add client handling for the server end.
  • Loading branch information...
commit 6643ee2c0ea04d1f0d13db8824fcb194789a788f 1 parent b468181
@rakaur authored
View
36 etc/config.yml
@@ -1,18 +1,24 @@
+---
+logging: info
+certificate: etc/server.crt
+private_key: etc/server.key
+
+# host:port
listen:
- host: 0.0.0.0
- port: 6667
- maxconnections: 10
+- *:4010
users:
-- name: example
- password: password
- networks:
- - name: malkier
- servers: irc.malkier.net:6667
- channels:
- - "#malkier"
- - "#lobby"
- - name: freenode
- servers: chat.freenode.net:6667
- channels: "#freenode"
-
+- example_user:
+ password: password
+
+ networks:
+ - malkier:
+ servers: irc.malkier.net:6667
+ channels:
+ - "#malkier"
+ - "#lobby"
+
+ - freenode:
+ servers: chat.freenode.net:6667
+ channels:
+ - "#freenode"
View
105 lib/rhubnc.rb
@@ -2,16 +2,10 @@
# rhubnc: rhuidean-based IRC bouncer
# lib/rhubnc.rb: startup routines, etc
#
-# Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
+# Copyright (c) 2003-2011 Eric Will <rakaur@malkier.net>
#
# encoding: utf-8
-# Import required Ruby modules
-%w(logger optparse yaml).each { |m| require m }
-
-# Import required application modules
-#%w().each { |m| require 'rhubnc/' + m }
-
# Check for rhuidean
begin
require 'rhuidean'
@@ -20,16 +14,20 @@
puts 'rhubnc: this library is required for IRC communication'
puts 'rhubnc: gem install --remote rhuidean'
abort
-else
- require 'rhuidean/stateful_client'
end
+# Import required Ruby modules
+%w(logger openssl optparse yaml).each { |m| require m }
+
+# Import required application modules
+%w(server).each { |m| require 'rhubnc/' + m }
+
# The main application class
class Bouncer
##
# mixins
- include Loggable # Magic logging from rhuidean
+ include Loggable # Magic logging from rhuidean
##
# constants
@@ -44,11 +42,17 @@ class Bouncer
VERSION = "#{V_MAJOR}.#{V_MINOR}.#{V_PATCH}"
+ ##
+ # class variables
+
# Configuration data
@@config = nil
- # Debug mode?
- @@debug = false
+ # A list of our servers
+ @@servers = []
+
+ # The OpenSSL context used for STARTTLS
+ @@ssl_context = nil
##
# Create a new +Bouncer+ object, which starts and runs the entire
@@ -57,10 +61,6 @@ class Bouncer
# return:: self
#
def initialize
-
- # Our logger
- @logger = nil
-
rhu = Rhuidean::VERSION
puts "#{ME}: version #{VERSION} (rhuidean-#{rhu}) [#{RUBY_PLATFORM}]"
@@ -83,8 +83,10 @@ def initialize
# Some defaults for state
logging = true
+ debug = false
willfork = RUBY_PLATFORM =~ /win32/i ? false : true
wd = Dir.getwd
+ @logger = nil
# Do command-line options
opts = OptionParser.new
@@ -95,7 +97,7 @@ def initialize
qd = 'Disable regular logging.'
vd = 'Display version information.'
- opts.on('-d', '--debug', dd) { @@debug = true }
+ opts.on('-d', '--debug', dd) { debug = true }
opts.on('-h', '--help', hd) { puts opts; abort }
opts.on('-n', '--no-fork', nd) { willfork = false }
opts.on('-q', '--quiet', qd) { logging = false }
@@ -109,7 +111,7 @@ def initialize
end
# Interpreter warnings
- $-w = true if @@debug
+ $-w = true if debug
# Signal handlers
trap(:INT) { app_exit }
@@ -132,15 +134,41 @@ def initialize
else
@@config = indifferent_hash(@@config)
- @nickname = @@config[:nickname]
-
if @@config[:die]
puts "#{ME}: you didn't read your config..."
exit
end
+
+ unless @@config[:listen]
+ puts "#{ME}: configure error: no listeners defined"
+ abort
+ end
end
- if @@debug
+ # Set up the SSL stuff - XXX
+ #certfile = @@config[:certificate]
+ #keyfile = @@config[:private_key]
+
+ #begin
+ # cert = OpenSSL::X509::Certificate.new(File.read(certfile))
+ # pkey = OpenSSL::PKey::RSA.new(File.read(keyfile))
+ #rescue Exception => e
+ # puts "#{ME}: configuration error: #{e}"
+ # abort
+ #else
+ # ctx = OpenSSL::SSL::SSLContext.new
+ # ctx.cert = cert
+ # ctx.key = pkey
+
+ # ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ # ctx.options = OpenSSL::SSL::OP_NO_TICKET
+ # ctx.options |= OpenSSL::SSL::OP_NO_SSLv2
+ # ctx.options |= OpenSSL::SSL::OP_ALL
+
+ # @@ssl_context = ctx
+ #end
+
+ if debug
puts "#{ME}: warning: debug mode enabled"
puts "#{ME}: warning: everything will be logged in the clear!"
end
@@ -182,7 +210,7 @@ def initialize
[$stdin, $stdout, $stderr].each { |s| s.close }
# Set up logging
- if logging or @@debug
+ if logging or debug
self.logger = Logger.new('var/rhubnc.log', 'weekly')
end
else
@@ -190,13 +218,13 @@ def initialize
puts "#{ME}: running in foreground mode from #{Dir.getwd}"
# Set up logging
- self.logger = Logger.new($stdout) if logging or @@debug
+ self.logger = Logger.new($stdout) if logging or debug
end
- if @@debug
+ if debug
log_level = :debug
- #else
- # log_level = @@config[:logging].to_sym
+ else
+ log_level = @@config[:logging].to_sym
end
self.log_level = log_level if logging
@@ -205,14 +233,25 @@ def initialize
Dir.mkdir('var') unless Dir.exists?('var')
File.open('var/rhubnc.pid', 'w') { |f| f.puts(Process.pid) }
- # Set up our handlers
- #set_event_handlers
+ # XXX - timers
+
+ # Start the listeners
+
+ @@config[:listen].each do |hostport|
+ bind_to, port = hostport.split(':')
+
+ @@servers << Server.new do |s|
+ s.bind_to = bind_to
+ s.port = port
+ s.logger = @logger if logging
+ end
+ end
# Start your engines...
- Thread.abort_on_exception = true if @@debug
+ Thread.abort_on_exception = true if debug
- #@@clients.each { |c| c.thread = Thread.new { c.io_loop } }
- #@@clients.each { |c| c.thread.join }
+ @@servers.each { |s| s.thread = Thread.new { s.io_loop } }
+ @@servers.each { |s| s.thread.join }
# Exiting...
app_exit
@@ -229,10 +268,6 @@ def Bouncer.config
@@config
end
- def Bouncer.debug
- @@debug
- end
-
#######
private
#######
View
182 lib/rhubnc/client.rb
@@ -0,0 +1,182 @@
+#
+# rhubnc: rhuidean-based IRC bouncer
+# lib/rhubnc/client.rb: represents a connected client
+#
+# Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
+#
+# encoding: utf-8
+
+# Import required Ruby modules
+%w(digest/md5 openssl).each { |m| require m } # XXX - SSL
+
+class Client
+
+ ##
+ # mixins
+ include Loggable
+
+ ##
+ # instance attributes
+ attr_reader :host, :resource, :socket
+
+ ##
+ # XXX
+ def initialize(host, socket)
+ # The hostname our client connected to
+ @connect_host = nil
+
+ # Is our socket dead?
+ @dead = false
+
+ # Our event queue
+ @eventq = IRC::EventQueue.new
+
+ # Our hostname
+ @host = host
+
+ # Our Logger object
+ @logger = nil
+ self.logger = nil
+
+ # Received data waiting to be parsed
+ @recvq = []
+
+ # Data waiting to be sent
+ @sendq = []
+
+ # Our socket
+ @socket = socket
+
+ # Our DB user object
+ @user = nil
+
+ # If we have a block let it set up our instance attributes
+ yield(self) if block_given?
+
+ # Set up event handlers
+ set_default_handlers
+
+ log(:debug, "new client from #@host")
+
+ self
+ end
+
+ #######
+ private
+ #######
+
+ def set_default_handlers
+ @eventq.handle(:recvq_ready) { parse }
+ end
+
+
+ def parse
+ # XXX gotta write this!
+ while line = @recvq.shift
+ line.chomp!
+ log(:debug,"-> #{line}")
+ end
+ end
+
+ #
+ # Takes care of setting some stuff when we die.
+ # ---
+ # bool:: +true+ or +false+
+ # returns:: +nil+
+ #
+ def dead=(bool)
+ if bool
+ # Try to flush the sendq first. This is for errors and such.
+ write unless @sendq.empty?
+
+ log(:info, "client from #@host disconnected")
+
+ @socket.close
+ @socket = nil
+ @dead = true
+ @state = []
+ end
+ end
+
+ ######
+ public
+ ######
+
+ def need_write?
+ @sendq.empty? ? false : true
+ end
+
+ def has_events?
+ @eventq.needs_ran?
+ end
+
+ def run_events
+ @eventq.run
+ end
+
+ def dead?
+ @dead
+ end
+
+ #
+ # Called when we're ready to read.
+ # ---
+ # returns:: +self+
+ #
+ def read
+ begin
+ ret = @socket.read_nonblock(8192)
+ rescue IO::WaitReadable
+ retry
+ rescue Exception => e
+ ret = nil # Dead
+ end
+
+ if not ret or ret.empty?
+ log(:info, "error from #@host: #{e}") if e
+ self.dead = true
+ return
+ end
+
+ # This passes every "line" to our block, including the "\n".
+ ret.scan(/(.+\n?)/) do |line|
+ line = line[0]
+
+ # If the last line had no \n, add this one onto it.
+ if @recvq[-1] and @recvq[-1][-1].chr != "\n"
+ @recvq[-1] += line
+ else
+ @recvq << line
+ end
+ end
+
+ if @recvq[-1] and @recvq[-1][-1].chr == "\n"
+ @eventq.post(:recvq_ready)
+ end
+
+ self
+ end
+
+ ##
+ # Called when we're ready to write.
+ # ---
+ # returns:: +self+
+ #
+ def write
+ # Use shift because we need it to fall off immediately.
+ while line = @sendq.shift
+ begin
+ line += "\r\n"
+ @socket.write_nonblock(line)
+ rescue IO::WaitReadable
+ retry
+ rescue Exception
+ self.dead = true
+ return
+ else
+ log(:debug, "< #{line[0 ... -2]}")
+ end
+ end
+ end
+end
+
View
177 lib/rhubnc/server.rb
@@ -0,0 +1,177 @@
+#
+# rhubnc: rhuidean-based IRC bouncer
+# lib/rhubnc/server.rb: acts as a TCP server
+#
+# Copyright (c) 2003-2011 Eric Will <rakaur@malkier.net>
+#
+# encoding: utf-8
+
+# Import required Ruby modules
+%w(logger socket).each { |m| require m }
+
+# Import required application modules
+%w(client).each { |m| require 'rhubnc/' + m }
+
+# This class acts as a TCP server and handles all clients connected to it.
+class Server
+ ##
+ # mixins
+ include Loggable
+
+ ##
+ # instance attributes
+ attr_accessor :thread
+ attr_reader :socket
+ attr_writer :bind_to, :port
+
+ ##
+ # Creates a new Server that listens for client connections.
+ # If given a block, it passes itself to the block for pretty
+ # attribute setting.
+ #
+ def initialize
+ # A list of our connected clients
+ @clients = []
+
+ # Is our socket dead?
+ @dead = false
+
+ # Our event queue
+ @eventq = IRC::EventQueue.new
+
+ # Our Logger object
+ @logger = nil
+ self.logger = nil
+
+ # If we have a block let it set up our instance attributes
+ yield(self) if block_given?
+
+ log(:debug, "new server at #@bind_to:#@port")
+
+ # Start up the listener
+ start_listening
+
+ # Set up event handlers
+ set_default_handlers
+
+ self
+ end
+
+ #######
+ private
+ #######
+
+ def start_listening
+ begin
+ if @bind_to == '*'
+ @socket = TCPServer.new(@port)
+ else
+ @socket = TCPServer.new(@bind_to, @port)
+ end
+ rescue Exception => e
+ log(:fatal, "error acquiring socket for #@bind_to:#@port")
+ raise
+ else
+ log(:info, "server listening at #@bind_to:#@port")
+ @dead = false
+ end
+ end
+
+ ##
+ # Sets up some default event handlers to track various states and such.
+ # ---
+ # returns:: +self+
+ #
+ def set_default_handlers
+ @eventq.handle(:dead) { start_listening }
+ @eventq.handle(:connection) { new_connection }
+
+ @eventq.handle(:read_ready) { |*args| client_read(*args) }
+ @eventq.handle(:write_ready) { |*args| client_write(*args) }
+ end
+
+ def new_connection
+ begin
+ newsock = @socket.accept_nonblock
+ rescue IO::WaitReadable
+ return
+ end
+
+ # This is to get around some silly IPv6 stuff
+ host = newsock.peeraddr[3].sub('::ffff:', '')
+
+ log(:info, "#@bind_to:#@port: new connection from #{host}")
+
+ @clients << Client.new(host, newsock) do |c|
+ c.logger = @logger
+ end
+ end
+
+ def client_read(socket)
+ @clients.find { |client| client.socket == socket }.read
+ end
+
+ def client_write(socket)
+ @clients.find { |client| client.socket == socket }.write
+ end
+
+ ######
+ public
+ ######
+
+ def dead?
+ @dead
+ end
+
+ def io_loop
+ loop do
+ # Is our server's listening socket dead?
+ if dead?
+ log(:warning, "listener has died on #@host:#@port, restarting")
+ @socket.close
+ @socket = nil
+ @eventq.post(:dead)
+ end
+
+ # Run the event loop. These events will add IO, and possibly other
+ # events, so we keep running until it's empty.
+ @eventq.run while @eventq.needs_ran?
+
+ # Run our client's event loops. Same deal as before.
+ @clients.each do |client|
+ client.run_events while client.has_events?
+ end
+
+ # Are any of our clients dead?
+ @clients.delete_if { |client| client.dead? }
+
+ # Get our sockets in order
+ readfds = [@socket]
+ writefds = []
+
+ @clients.each do |client|
+ readfds << client.socket
+ writefds << client.socket if client.need_write?
+ end
+
+ ret = IO.select(readfds, writefds, nil, nil)
+
+ next unless ret # Nothing to do
+
+ # readfds
+ ret[0].each do |socket|
+ if socket == @socket
+ @eventq.post(:connection)
+ else
+ @eventq.post(:read_ready, socket)
+ end
+ end unless ret[0].empty?
+
+ # writefds
+ ret[1].each do |socket|
+ @eventq.post(:write_ready, socket)
+ end unless ret[1].empty?
+ end
+ end
+end
+
Please sign in to comment.
Something went wrong with that request. Please try again.