tkellem 0.9.0.beta3 → 0.9.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,12 @@
1
1
  require 'active_support/core_ext/class/attribute_accessors'
2
- require 'celluloid/io'
3
2
 
3
+ require 'tkellem/irc_server'
4
4
  require 'tkellem/bouncer_connection'
5
5
 
6
6
  module Tkellem
7
7
 
8
8
  class Bouncer
9
- include Celluloid::IO
10
9
  include Tkellem::EasyLogger
11
- include Tkellem::CelluloidTools::LineReader
12
10
 
13
11
  attr_reader :user, :network, :nick, :network_user, :connected_at
14
12
  cattr_accessor :plugins
@@ -33,7 +31,7 @@ class Bouncer
33
31
  # clients waiting for us to connect to the irc server
34
32
  @waiting_clients = []
35
33
 
36
- async.connect
34
+ connect!
37
35
  end
38
36
 
39
37
  def data(key)
@@ -104,13 +102,6 @@ class Bouncer
104
102
  end
105
103
  end
106
104
 
107
- def receive_line(line)
108
- trace "from server: #{line}"
109
- return if line.blank?
110
- msg = IrcMessage.parse(line)
111
- server_msg(msg)
112
- end
113
-
114
105
  def server_msg(msg)
115
106
  return if plugins.any? do |plugin|
116
107
  !plugin.server_msg(self, msg)
@@ -187,27 +178,32 @@ class Bouncer
187
178
  alias_method :log_name, :name
188
179
 
189
180
  def send_msg(msg)
181
+ return unless @conn
190
182
  trace "to server: #{msg}"
191
- @socket.write("#{msg}\r\n")
183
+ @conn.send_data("#{msg}\r\n")
192
184
  end
193
185
 
194
- def connection_established
195
- debug "Connected"
186
+ def connection_established(conn)
187
+ @conn = conn
196
188
  # TODO: support sending a real username, realname, etc
197
189
  send_msg("USER #{@user.username} somehost tkellem :#{@user.name}@tkellem")
198
190
  change_nick(@nick, true)
199
191
  @connected_at = Time.now
200
- async.run
201
192
  end
202
193
 
203
194
  def disconnected!
204
195
  debug "OMG we got disconnected."
196
+ @conn = nil
205
197
  @connected = false
206
198
  @connected_at = nil
207
199
  @active_conns.each { |c,s| c.close_connection }
208
200
  connect!
209
201
  end
210
202
 
203
+ def kill!
204
+ @active_conns.each { |c,s| c.close_connection }
205
+ end
206
+
211
207
  protected
212
208
 
213
209
  def change_nick(new_nick, force = false)
@@ -220,21 +216,9 @@ class Bouncer
220
216
  @welcomes.each { |msg| msg.args[0] = nick; bouncer_conn.send_msg(msg) }
221
217
  end
222
218
 
223
- def connect
224
- hosts = network.reload.hosts
225
- # random rather than round-robin, not totally ideal
226
- # Note: Celluloid::TCPSocket also is random rather than round-robin when
227
- # picking the DNS IP record to connect to
228
- host = hosts[rand(hosts.length)]
229
- begin
230
- info "Connecting to #{host}"
231
- @socket = TCPSocket.new(host.address, host.port)
232
- if host.ssl
233
- @socket = SSLSocket.new(@socket)
234
- @socket.connect
235
- end
236
- end
237
- connection_established
219
+ def connect!
220
+ @connector ||= IrcServerConnection.connector(self, network)
221
+ @connector.connect!
238
222
  end
239
223
 
240
224
  def ready!
@@ -1,19 +1,19 @@
1
1
  require 'active_support/core_ext/object/blank'
2
- require 'celluloid/io'
3
2
 
3
+ require 'eventmachine'
4
4
  require 'tkellem/irc_message'
5
- require 'tkellem/celluloid_tools'
6
5
 
7
6
  module Tkellem
8
7
 
9
- class BouncerConnection
10
- include Celluloid::IO
8
+ module BouncerConnection
9
+ include EM::Protocols::LineText2
11
10
  include Tkellem::EasyLogger
12
- include Tkellem::CelluloidTools::LineReader
13
11
 
14
- def initialize(tkellem_server, socket)
12
+ def initialize(tkellem_server, do_ssl)
13
+ set_delimiter "\r\n"
14
+
15
+ @ssl = do_ssl
15
16
  @tkellem = tkellem_server
16
- @socket = socket
17
17
 
18
18
  @state = :auth
19
19
  @name = 'new-conn'
@@ -31,11 +31,24 @@ class BouncerConnection
31
31
  @data[key] ||= {}
32
32
  end
33
33
 
34
+ def post_init
35
+ failsafe(:post_init) do
36
+ if ssl
37
+ start_tls :verify_peer => false
38
+ else
39
+ ssl_handshake_completed
40
+ end
41
+ end
42
+ end
43
+
44
+ def ssl_handshake_completed
45
+ end
46
+
34
47
  def error!(msg)
35
48
  info("ERROR :#{msg}")
36
49
  say_as_tkellem(msg)
37
50
  send_msg("ERROR :#{msg}")
38
- close_connection
51
+ close_connection(true)
39
52
  end
40
53
 
41
54
  def connect_to_irc_server
@@ -77,38 +90,40 @@ class BouncerConnection
77
90
  end
78
91
 
79
92
  def receive_line(line)
80
- trace "from client: #{line}"
81
- return if line.blank?
82
- msg = IrcMessage.parse(line)
83
-
84
- command = msg.command
85
- if @state != :auth && command == 'PRIVMSG' && msg.args.first == '-tkellem'
86
- msg_tkellem(IrcMessage.new(nil, 'TKELLEM', [msg.args.last]))
87
- elsif command == 'TKELLEM' || command == 'TK'
88
- msg_tkellem(msg)
89
- elsif command == 'CAP'
90
- # TODO: full support for CAP -- this just gets mobile colloquy connecting
91
- if msg.args.first =~ /req/i
92
- send_msg("CAP NAK")
93
- end
94
- elsif command == 'PASS' && @state == :auth
95
- @password = msg.args.first
96
- elsif command == 'NICK' && @state == :auth
97
- @connecting_nick = msg.args.first
98
- maybe_connect
99
- elsif command == 'QUIT'
100
- close_connection
101
- elsif command == 'USER' && @state == :auth
102
- unless @username
103
- @username, @conn_info = msg.args.first.strip.split('@', 2).map { |a| a.downcase }
93
+ failsafe("message: {#{line}}") do
94
+ trace "from client: #{line}"
95
+ return if line.blank?
96
+ msg = IrcMessage.parse(line)
97
+
98
+ command = msg.command
99
+ if @state != :auth && command == 'PRIVMSG' && msg.args.first == '-tkellem'
100
+ msg_tkellem(IrcMessage.new(nil, 'TKELLEM', [msg.args.last]))
101
+ elsif command == 'TKELLEM' || command == 'TK'
102
+ msg_tkellem(msg)
103
+ elsif command == 'CAP'
104
+ # TODO: full support for CAP -- this just gets mobile colloquy connecting
105
+ if msg.args.first =~ /req/i
106
+ send_msg("CAP NAK")
107
+ end
108
+ elsif command == 'PASS' && @state == :auth
109
+ @password = msg.args.first
110
+ elsif command == 'NICK' && @state == :auth
111
+ @connecting_nick = msg.args.first
112
+ maybe_connect
113
+ elsif command == 'QUIT'
114
+ close_connection
115
+ elsif command == 'USER' && @state == :auth
116
+ unless @username
117
+ @username, @conn_info = msg.args.first.strip.split('@', 2).map { |a| a.downcase }
118
+ end
119
+ maybe_connect
120
+ elsif @state == :auth
121
+ error!("Protocol error. You must authenticate first.")
122
+ elsif @state == :connected
123
+ @bouncer.client_msg(self, msg)
124
+ else
125
+ say_as_tkellem("You must connect to an irc network to do that.")
104
126
  end
105
- maybe_connect
106
- elsif @state == :auth
107
- error!("Protocol error. You must authenticate first.")
108
- elsif @state == :connected
109
- @bouncer.client_msg(self, msg)
110
- else
111
- say_as_tkellem("You must connect to an irc network to do that.")
112
127
  end
113
128
  end
114
129
 
@@ -182,13 +197,14 @@ class BouncerConnection
182
197
  end
183
198
 
184
199
  def send_msg(msg)
185
- return if @socket.closed?
186
200
  trace "to client: #{msg}"
187
- @socket.write("#{msg}\r\n")
201
+ send_data("#{msg}\r\n")
188
202
  end
189
203
 
190
204
  def unbind
191
- @bouncer.try(:disconnect_client, self)
205
+ failsafe(:unbind) do
206
+ @bouncer.disconnect_client(self) if @bouncer
207
+ end
192
208
  end
193
209
  end
194
210
 
@@ -2,6 +2,7 @@ require 'fileutils'
2
2
  require 'optparse'
3
3
 
4
4
  require 'tkellem'
5
+ require 'tkellem/socket_server'
5
6
 
6
7
  class Tkellem::Daemon
7
8
  attr_reader :options
@@ -14,7 +15,7 @@ class Tkellem::Daemon
14
15
  end
15
16
 
16
17
  def run
17
- opts = OptionParser.new do |opts|
18
+ op = OptionParser.new do |opts|
18
19
  opts.banner = "Usage #{$0} <command> <options>"
19
20
  opts.separator %{\nWhere <command> is one of:
20
21
  start start the jobs daemon
@@ -29,13 +30,14 @@ class Tkellem::Daemon
29
30
  opts.on("-p", "--path", "Use alternate folder for tkellem data (default #{options[:path]})") { |p| options[:path] = p }
30
31
  opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
31
32
  end
32
- opts.parse!(@args)
33
+ op.parse!(@args)
33
34
 
34
35
  FileUtils.mkdir_p(path)
35
36
  File.chmod(0700, path)
36
37
  command = @args.shift
37
38
  case command
38
39
  when 'start'
40
+ abort_if_running
39
41
  daemonize
40
42
  start
41
43
  when 'stop'
@@ -85,8 +87,12 @@ class Tkellem::Daemon
85
87
  end
86
88
 
87
89
  def start
90
+ trap("INT") { EM.stop }
88
91
  remove_files
89
- Tkellem::TkellemServer.new(options).run
92
+ EM.run do
93
+ @admin = EM.start_unix_domain_server(socket_file, Tkellem::SocketServer)
94
+ Tkellem::TkellemServer.new
95
+ end
90
96
  ensure
91
97
  remove_files
92
98
  end
@@ -99,9 +105,6 @@ class Tkellem::Daemon
99
105
  @daemon = true
100
106
  File.open(pid_file, 'wb') { |f| f.write(Process.pid.to_s) }
101
107
 
102
- # TODO: support syslog
103
- require 'logger'
104
- logger = Logger.new(log_file)
105
108
  STDIN.reopen("/dev/null")
106
109
  STDOUT.reopen(log_file, 'a')
107
110
  STDERR.reopen(STDOUT)
@@ -133,8 +136,16 @@ class Tkellem::Daemon
133
136
  pid.to_i > 0 ? pid.to_i : nil
134
137
  end
135
138
 
139
+ def abort_if_running
140
+ pid = File.read(pid_file) if File.file?(pid_file)
141
+ if pid.to_i > 0
142
+ puts "tkellem already running, pid: #{pid}"
143
+ exit
144
+ end
145
+ end
146
+
136
147
  def remove_files
137
- FileUtils.rm(socket_file) if File.exist?(socket_file)
148
+ FileUtils.rm(socket_file) if File.file?(socket_file)
138
149
  return unless @daemon
139
150
  pid = File.read(pid_file) if File.file?(pid_file)
140
151
  if pid.to_i == Process.pid
@@ -0,0 +1,124 @@
1
+ require 'eventmachine'
2
+ require 'set'
3
+ require 'socket'
4
+
5
+ require 'tkellem/irc_message'
6
+ require 'tkellem/bouncer_connection'
7
+
8
+ module Tkellem
9
+
10
+ module IrcServerConnection
11
+ include EM::Protocols::LineText2
12
+
13
+ def initialize(connection_state, bouncer, do_ssl)
14
+ set_delimiter "\r\n"
15
+ @bouncer = bouncer
16
+ @ssl = do_ssl
17
+ @connection_state = connection_state
18
+ @connected = false
19
+ end
20
+
21
+ def connection_completed
22
+ if @ssl
23
+ @bouncer.failsafe(:connection_completed) do
24
+ @bouncer.debug "starting TLS"
25
+ # TODO: support strict cert checks
26
+ start_tls :verify_peer => false
27
+ end
28
+ else
29
+ ssl_handshake_completed
30
+ end
31
+ end
32
+
33
+ def ssl_handshake_completed
34
+ @bouncer.failsafe(:ssl_handshake_completed) do
35
+ @connected = true
36
+ @bouncer.connection_established(self)
37
+ end
38
+ end
39
+
40
+ def receive_line(line)
41
+ @bouncer.failsafe(:receive_line) do
42
+ @bouncer.trace "from server: #{line}"
43
+ return if line.blank?
44
+ msg = IrcMessage.parse(line)
45
+ @bouncer.server_msg(msg)
46
+ end
47
+ end
48
+
49
+ def unbind
50
+ @bouncer.failsafe(:unbind) do
51
+ if @connected
52
+ @bouncer.disconnected!
53
+ else
54
+ @bouncer.debug "Connection failed, trying next"
55
+ @connection_state.connect!
56
+ end
57
+ end
58
+ end
59
+
60
+ class ConnectionState < Struct.new(:bouncer, :network, :attempted, :getting)
61
+ def initialize(bouncer, network)
62
+ super(bouncer, network, Set.new, false)
63
+ reset
64
+ end
65
+
66
+ def connect!
67
+ raise("already in the process of getting an address") if getting
68
+ self.getting = true
69
+ network.reload
70
+ host_infos = network.hosts.map { |h| h.attributes }
71
+ EM.defer(proc { find_address(host_infos) }, method(:got_address))
72
+ end
73
+
74
+ def reset
75
+ self.attempted.clear
76
+ end
77
+
78
+ # runs in threadpool
79
+ def find_address(hosts)
80
+ candidates = Set.new
81
+ hosts.each do |host|
82
+ Socket.getaddrinfo(host['address'], host['port'], Socket::AF_INET, Socket::SOCK_STREAM, Socket::IPPROTO_TCP).each do |found|
83
+ candidates << [found[3], host['port'], host['ssl']]
84
+ end
85
+ end
86
+
87
+ to_try = candidates.to_a.sort_by { rand }.find { |c| !attempted.include?(c) }
88
+ if to_try.nil?
89
+ # we've tried all possible hosts, start over
90
+ return nil
91
+ end
92
+
93
+ return to_try
94
+ end
95
+
96
+ # back on event thread
97
+ def got_address(to_try)
98
+ self.getting = false
99
+
100
+ if !to_try
101
+ # sleep for a bit and try again
102
+ bouncer.debug "All available addresses failed, sleeping 5s and then trying over"
103
+ reset
104
+ EM.add_timer(5) { connect! }
105
+ return
106
+ end
107
+
108
+ attempted << to_try
109
+ address, port, ssl = to_try
110
+
111
+ bouncer.debug "Connecting to: #{Host.address_string(address, port, ssl)}"
112
+ bouncer.failsafe("connect: #{Host.address_string(address, port, ssl)}") do
113
+ EM.connect(address, port, IrcServerConnection, self, bouncer, ssl)
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.connector(bouncer, network)
119
+ ConnectionState.new(bouncer, network)
120
+ end
121
+
122
+ end
123
+
124
+ end
@@ -1,50 +1,33 @@
1
- Sequel.migration do
2
- up do
3
- if self.table_exists?(:schema_migrations) && self[:schema_migrations].first(:version => '1')
4
- next
1
+ class InitDb < ActiveRecord::Migration
2
+ def self.up
3
+ create_table 'listen_addresses' do |t|
4
+ t.string 'address', :null => false
5
+ t.integer 'port', :null => false
6
+ t.boolean 'ssl', :null => false, :default => false
5
7
  end
6
8
 
7
- create_table 'listen_addresses' do
8
- primary_key :id
9
- String 'address', :null => false
10
- Integer 'port', :null => false
11
- boolean 'ssl', :null => false, :default => false
9
+ create_table 'users' do |t|
10
+ t.string 'username', :null => false
11
+ t.string 'password'
12
+ t.string 'role', :null => false, :default => 'user'
12
13
  end
13
14
 
14
- create_table 'users' do
15
- primary_key :id
16
- String 'username', :null => false
17
- String 'password'
18
- String 'role', :null => false, :default => 'user'
15
+ create_table 'networks' do |t|
16
+ t.belongs_to 'user'
17
+ t.string 'name', :null => false
19
18
  end
20
19
 
21
- create_table 'networks' do
22
- primary_key :id
23
- foreign_key :user_id, :users
24
- String 'name', :null => false
20
+ create_table 'hosts' do |t|
21
+ t.belongs_to 'network'
22
+ t.string 'address', :null => false
23
+ t.integer 'port', :null => false
24
+ t.boolean 'ssl', :null => false, :default => false
25
25
  end
26
26
 
27
- create_table 'hosts' do
28
- primary_key :id
29
- foreign_key :network_id, :networks
30
- String 'address', :null => false
31
- Integer 'port', :null => false
32
- boolean 'ssl', :null => false, :default => false
27
+ create_table 'network_users' do |t|
28
+ t.belongs_to 'user'
29
+ t.belongs_to 'network'
30
+ t.string 'nick'
33
31
  end
34
-
35
- create_table 'network_users' do
36
- primary_key :id
37
- foreign_key :user_id, :users
38
- foreign_key :network_id, :networks
39
- String 'nick'
40
- end
41
- end
42
-
43
- down do
44
- drop_table 'network_users'
45
- drop_table 'hosts'
46
- drop_table 'networks'
47
- drop_table 'users'
48
- drop_table 'listen_addresses'
49
32
  end
50
33
  end
@@ -1,24 +1,7 @@
1
- Sequel.migration do
2
- up do
3
- if self.table_exists?(:schema_migrations) && self[:schema_migrations].first(:version => '2')
4
- next
5
- end
6
-
7
- alter_table 'networks' do
8
- add_column 'at_connect', String, :text => true
9
- end
10
- alter_table 'network_users' do
11
- add_column 'at_connect', String, :text => true
12
- end
13
- end
14
-
15
- down do
16
- alter_table 'network_users' do
17
- drop_column 'at_connect'
18
- end
19
- alter_table 'networks' do
20
- drop_column 'at_connect'
21
- end
1
+ class AtConnectColumns < ActiveRecord::Migration
2
+ def self.up
3
+ add_column 'networks', 'at_connect', 'text'
4
+ add_column 'network_users', 'at_connect', 'text'
22
5
  end
23
6
  end
24
7
 
@@ -1,22 +1,13 @@
1
- Sequel.migration do
2
- up do
3
- if self.table_exists?(:schema_migrations) && self[:schema_migrations].first(:version => '3')
4
- next
1
+ class Settings < ActiveRecord::Migration
2
+ def self.up
3
+ create_table 'settings' do |t|
4
+ t.string :name, :null => false
5
+ t.string :value, :null => false
6
+ t.boolean :unchanged, :null => false, :default => true
5
7
  end
6
8
 
7
- create_table 'settings' do
8
- primary_key :id
9
- String :name, :null => false
10
- String :value, :null => false
11
- boolean :unchanged, :null => false, :default => true
12
- end
13
-
14
- self[:settings].insert(:name => 'user_registration', :value => 'closed')
15
- self[:settings].insert(:name => 'recaptcha_api_key', :value => '')
16
- self[:settings].insert(:name => 'allow_user_networks', :value => 'false')
17
- end
18
-
19
- down do
20
- drop_table 'settings'
9
+ Tkellem::Setting.make_new('user_registration', 'closed')
10
+ Tkellem::Setting.make_new('recaptcha_api_key', '')
11
+ Tkellem::Setting.make_new('allow_user_networks', 'false')
21
12
  end
22
13
  end
@@ -1,7 +1,7 @@
1
1
  module Tkellem
2
2
 
3
- class Host < Sequel::Model
4
- many_to_one :network
3
+ class Host < ActiveRecord::Base
4
+ belongs_to :network
5
5
 
6
6
  def to_s
7
7
  self.class.address_string(address, port, ssl)
@@ -1,24 +1,12 @@
1
1
  module Tkellem
2
2
 
3
- class ListenAddress < Sequel::Model
4
- plugin :validation_class_methods
5
-
3
+ class ListenAddress < ActiveRecord::Base
6
4
  validates_uniqueness_of :port, :scope => [:address]
7
5
  validates_presence_of :address, :port
8
6
 
9
7
  def to_s
10
8
  "#{ssl ? 'ircs' : 'irc'}://#{address}:#{port}"
11
9
  end
12
-
13
- def after_create
14
- super
15
- $tkellem_server.try(:after_create, self)
16
- end
17
-
18
- def after_destroy
19
- super
20
- $tkellem_server.try(:after_destroy, self)
21
- end
22
10
  end
23
11
 
24
12
  end
@@ -1,24 +1,20 @@
1
1
  module Tkellem
2
2
 
3
- class Network < Sequel::Model
4
- plugin :nested_attributes
5
- plugin :validation_class_methods
6
- plugin :serialization
3
+ class Network < ActiveRecord::Base
4
+ has_many :hosts, :dependent => :destroy
5
+ accepts_nested_attributes_for :hosts
7
6
 
8
- one_to_many :hosts, :dependent => :destroy
9
- nested_attributes :hosts
10
-
11
- one_to_many :network_users, :dependent => :destroy
7
+ has_many :network_users, :dependent => :destroy
12
8
  # networks either belong to a specific user, or they are public and any user
13
9
  # can join them.
14
- many_to_one :user
10
+ belongs_to :user
15
11
 
16
12
  validates_uniqueness_of :name, :scope => :user_id
17
13
 
18
- serialize_attributes :yaml, :at_connect
14
+ serialize :at_connect, Array
19
15
 
20
16
  def at_connect
21
- super || []
17
+ read_attribute(:at_connect) || []
22
18
  end
23
19
 
24
20
  def public?
@@ -1,33 +1,17 @@
1
1
  module Tkellem
2
2
 
3
- class NetworkUser < Sequel::Model
4
- plugin :serialization
3
+ class NetworkUser < ActiveRecord::Base
4
+ belongs_to :network
5
+ belongs_to :user
5
6
 
6
- many_to_one :network
7
- many_to_one :user
8
-
9
- serialize_attributes :yaml, :at_connect
10
-
11
- def at_connect
12
- super || []
13
- end
7
+ serialize :at_connect, Array
14
8
 
15
9
  def nick
16
- super || user.name
10
+ read_attribute(:nick) || user.name
17
11
  end
18
12
 
19
13
  def combined_at_connect
20
- network.at_connect + at_connect
21
- end
22
-
23
- def after_create
24
- super
25
- $tkellem_server.try(:after_create, self)
26
- end
27
-
28
- def after_destroy
29
- super
30
- $tkellem_server.try(:after_destroy, self)
14
+ network.at_connect + (at_connect || [])
31
15
  end
32
16
  end
33
17