tkellem 0.9.0.beta3 → 0.9.0.beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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