tkellem 0.8.11 → 0.9.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,4 +1,13 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+
5
+ gem 'celluloid', :git => 'git://github.com/celluloid/celluloid'
6
+ gem 'celluloid-io', :git => 'https://github.com/celluloid/celluloid-io.git'
7
+ gem 'activesupport', '3.2.10'
8
+ gem 'sequel', '3.42.0'
9
+ gem 'sqlite3', '1.3.6'
10
+ gem 'rspec', '2.5'
11
+
3
12
  # Specify your gem's dependencies in tkellem.gemspec
4
13
  gemspec
@@ -1,17 +1,21 @@
1
1
  require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'celluloid/io'
2
3
 
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
9
10
  include Tkellem::EasyLogger
11
+ include Tkellem::CelluloidTools::LineReader
10
12
 
11
13
  attr_reader :user, :network, :nick, :network_user, :connected_at
12
14
  cattr_accessor :plugins
13
15
  self.plugins = []
14
16
 
17
+ class Room < Struct.new(:name, :topic, :topic_setter, :topic_time); end
18
+
15
19
  def initialize(network_user)
16
20
  @network_user = network_user
17
21
  @user = network_user.user
@@ -21,7 +25,7 @@ class Bouncer
21
25
  # maps { client_conn => state_hash }
22
26
  @active_conns = {}
23
27
  @welcomes = []
24
- @rooms = []
28
+ @rooms = {}
25
29
  # maps { client_conn => away_status_or_nil }
26
30
  @away = {}
27
31
  # plugin data
@@ -29,7 +33,7 @@ class Bouncer
29
33
  # clients waiting for us to connect to the irc server
30
34
  @waiting_clients = []
31
35
 
32
- connect!
36
+ async.connect
33
37
  end
34
38
 
35
39
  def data(key)
@@ -62,7 +66,7 @@ class Bouncer
62
66
  client.send_msg(":#{client.connecting_nick} NICK #{nick}") if client.connecting_nick != nick
63
67
  send_welcome(client)
64
68
  # make the client join all the rooms that we're in
65
- @rooms.each { |room| client.simulate_join(room) }
69
+ @rooms.each_value { |room| client.simulate_join(room) }
66
70
 
67
71
  plugins.each { |plugin| plugin.new_client_connected(self, client) }
68
72
  check_away_status
@@ -100,6 +104,13 @@ class Bouncer
100
104
  end
101
105
  end
102
106
 
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
+
103
114
  def server_msg(msg)
104
115
  return if plugins.any? do |plugin|
105
116
  !plugin.server_msg(self, msg)
@@ -112,12 +123,25 @@ class Bouncer
112
123
  false
113
124
  when 'JOIN'
114
125
  debug "#{msg.target_user} joined #{msg.args.first}"
115
- @rooms << msg.args.first if msg.target_user == @nick
126
+ @rooms[msg.args.first] = Room.new(msg.args.first) if msg.target_user == @nick
116
127
  true
117
128
  when 'PART'
118
129
  debug "#{msg.target_user} left #{msg.args.first}"
119
130
  @rooms.delete(msg.args.first) if msg.target_user == @nick
120
131
  true
132
+ when 'TOPIC'
133
+ if room = @rooms[msg.args.first]
134
+ room.topic = msg.args.last
135
+ end
136
+ when '332' # topic replay
137
+ if room = @rooms[msg.args[1]]
138
+ room.topic = msg.args.last
139
+ end
140
+ when '333' # topic timestamp
141
+ if room = @rooms[msg.args[1]]
142
+ room.topic_setter = msg.args[2]
143
+ room.topic_time = msg.args[3]
144
+ end
121
145
  when 'PING'
122
146
  send_msg("PONG tkellem!tkellem :#{msg.args.last}")
123
147
  false
@@ -165,22 +189,21 @@ class Bouncer
165
189
  alias_method :log_name, :name
166
190
 
167
191
  def send_msg(msg)
168
- return unless @conn
169
192
  trace "to server: #{msg}"
170
- @conn.send_data("#{msg}\r\n")
193
+ @socket.write("#{msg}\r\n")
171
194
  end
172
195
 
173
- def connection_established(conn)
174
- @conn = conn
196
+ def connection_established
197
+ debug "Connected"
175
198
  # TODO: support sending a real username, realname, etc
176
199
  send_msg("USER #{@user.username} somehost tkellem :#{@user.name}@tkellem")
177
200
  change_nick(@nick, true)
178
201
  @connected_at = Time.now
202
+ async.run
179
203
  end
180
204
 
181
205
  def disconnected!
182
206
  debug "OMG we got disconnected."
183
- @conn = nil
184
207
  @connected = false
185
208
  @connected_at = nil
186
209
  @active_conns.each { |c,s| c.close_connection }
@@ -199,14 +222,26 @@ class Bouncer
199
222
  @welcomes.each { |msg| msg.args[0] = nick; bouncer_conn.send_msg(msg) }
200
223
  end
201
224
 
202
- def connect!
203
- @connector ||= IrcServerConnection.connector(self, network)
204
- @connector.connect!
225
+ def connect
226
+ hosts = network.reload.hosts
227
+ # random rather than round-robin, not totally ideal
228
+ # Note: Celluloid::TCPSocket also is random rather than round-robin when
229
+ # picking the DNS IP record to connect to
230
+ host = hosts[rand(hosts.length)]
231
+ begin
232
+ info "Connecting to #{host}"
233
+ @socket = TCPSocket.new(host.address, host.port)
234
+ if host.ssl
235
+ @socket = SSLSocket.new(@socket)
236
+ @socket.connect
237
+ end
238
+ end
239
+ connection_established
205
240
  end
206
241
 
207
242
  def ready!
208
- @rooms.each do |room|
209
- send_msg("JOIN #{room}")
243
+ @rooms.each_key do |room|
244
+ send_msg("JOIN #{room.name}")
210
245
  end
211
246
 
212
247
  check_away_status
@@ -1,19 +1,19 @@
1
1
  require 'active_support/core_ext/object/blank'
2
+ require 'celluloid/io'
2
3
 
3
- require 'eventmachine'
4
4
  require 'tkellem/irc_message'
5
+ require 'tkellem/celluloid_tools'
5
6
 
6
7
  module Tkellem
7
8
 
8
- module BouncerConnection
9
- include EM::Protocols::LineText2
9
+ class BouncerConnection
10
+ include Celluloid::IO
10
11
  include Tkellem::EasyLogger
12
+ include Tkellem::CelluloidTools::LineReader
11
13
 
12
- def initialize(tkellem_server, do_ssl)
13
- set_delimiter "\r\n"
14
-
15
- @ssl = do_ssl
14
+ def initialize(tkellem_server, socket)
16
15
  @tkellem = tkellem_server
16
+ @socket = socket
17
17
 
18
18
  @state = :auth
19
19
  @name = 'new-conn'
@@ -31,24 +31,11 @@ module 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
-
47
34
  def error!(msg)
48
35
  info("ERROR :#{msg}")
49
36
  say_as_tkellem(msg)
50
37
  send_msg("ERROR :#{msg}")
51
- close_connection(true)
38
+ close_connection
52
39
  end
53
40
 
54
41
  def connect_to_irc_server
@@ -90,40 +77,38 @@ module BouncerConnection
90
77
  end
91
78
 
92
79
  def receive_line(line)
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.")
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")
126
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 }
104
+ 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.")
127
112
  end
128
113
  end
129
114
 
@@ -188,21 +173,22 @@ module BouncerConnection
188
173
  end
189
174
 
190
175
  def simulate_join(room)
191
- send_msg(":#{nick} JOIN #{room}")
176
+ send_msg(":#{nick} JOIN #{room.name}")
192
177
  # TODO: intercept the NAMES response so that only this bouncer gets it
193
178
  # Otherwise other clients might show an "in this room" line.
194
- @bouncer.send_msg("NAMES #{room}\r\n")
179
+ @bouncer.send_msg("NAMES #{room.name}\r\n")
180
+ send_msg(IrcMessage.new(":tkellem", "332", [nick, room.name, room.topic])) if room.topic
181
+ send_msg(IrcMessage.new(":tkellem", "333", [nick, room.name, room.topic_setter, room.topic_time])) if room.topic_setter && room.topic_time
195
182
  end
196
183
 
197
184
  def send_msg(msg)
185
+ return if @socket.closed?
198
186
  trace "to client: #{msg}"
199
- send_data("#{msg}\r\n")
187
+ @socket.write("#{msg}\r\n")
200
188
  end
201
189
 
202
190
  def unbind
203
- failsafe(:unbind) do
204
- @bouncer.disconnect_client(self) if @bouncer
205
- end
191
+ @bouncer.try(:disconnect_client, self)
206
192
  end
207
193
  end
208
194
 
@@ -0,0 +1,131 @@
1
+ require 'openssl'
2
+ require 'celluloid/io'
3
+
4
+ module Tkellem
5
+ module CelluloidTools
6
+
7
+ # Generates a new SSL context with a new cert and key
8
+ # Great for easily getting up and running, but not necessarily a good idea for
9
+ # production use
10
+ def self.generate_ssl_ctx
11
+ key = OpenSSL::PKey::RSA.new(2048)
12
+
13
+ dn = OpenSSL::X509::Name.parse("/CN=tkellem-auto")
14
+ cert = OpenSSL::X509::Certificate.new
15
+ cert.version = 2
16
+ cert.serial = 1
17
+ cert.subject = dn
18
+ cert.issuer = dn
19
+ cert.public_key = key.public_key
20
+ cert.not_before = Time.now
21
+ cert.not_after = Time.now + 94670777 # 3 years
22
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
23
+
24
+ ctx = OpenSSL::SSL::SSLContext.new
25
+ ctx.key = key
26
+ ctx.cert = cert
27
+
28
+ ctx
29
+ end
30
+
31
+ class BackoffSupervisor < ::Celluloid::SupervisionGroup
32
+ attr_reader :registry
33
+
34
+ def restart_actor(actor, reason)
35
+ member = @members.find do |member|
36
+ member.actor == actor
37
+ end
38
+ raise "a group member went missing. This shouldn't be!" unless member
39
+
40
+ if reason
41
+ end
42
+
43
+ member.restart(reason)
44
+ end
45
+ end
46
+
47
+ class Listener < Struct.new(:server, :callback)
48
+ include Celluloid::IO
49
+
50
+ def self.start(*args, &callback)
51
+ listener = self.new(*args)
52
+ listener.callback = callback
53
+ listener.async.run
54
+ listener
55
+ end
56
+
57
+ def run
58
+ loop { handle_connection(server.accept) }
59
+ end
60
+
61
+ def handle_connection(socket)
62
+ callback.(socket)
63
+ end
64
+
65
+ finalizer :close
66
+
67
+ def close
68
+ server.try(:close) unless server.try(:closed?)
69
+ end
70
+ end
71
+
72
+ class TCPListener < Listener
73
+ def initialize(host, port)
74
+ self.server = TCPServer.new(host, port)
75
+ end
76
+ end
77
+
78
+ class SSLListener < Listener
79
+ def initialize(host, port)
80
+ self.server = SSLServer.new(TCPServer.new(host, port), CelluloidTools.generate_ssl_ctx)
81
+ end
82
+ end
83
+
84
+ class UnixListener < Listener
85
+ def initialize(socket_path)
86
+ self.server = UNIXServer.new(socket_path)
87
+ end
88
+ end
89
+
90
+ module LineReader
91
+ def self.included(k)
92
+ k.send :finalizer, :close_connection
93
+ end
94
+
95
+ def readline
96
+ @delimiter ||= "\r\n"
97
+ @readline_buffer ||= ''
98
+ loop do
99
+ if idx = @readline_buffer.index(@delimiter)
100
+ postidx = idx + @delimiter.size
101
+ line = @readline_buffer[0, postidx]
102
+ @readline_buffer = @readline_buffer[postidx..-1]
103
+ return line
104
+ else
105
+ @socket.readpartial(4096, @readline_buffer)
106
+ end
107
+ end
108
+ end
109
+
110
+ def close_connection
111
+ @socket.close if @socket && !@socket.closed?
112
+ end
113
+
114
+ def run
115
+ loop do
116
+ line = readline
117
+ receive_line(line)
118
+ end
119
+ rescue EOFError, IOError, OpenSSL::SSL::SSLError
120
+ unbind
121
+ end
122
+
123
+ def receive_line(line)
124
+ end
125
+
126
+ def unbind
127
+ end
128
+ end
129
+
130
+ end
131
+ end
@@ -2,7 +2,6 @@ require 'fileutils'
2
2
  require 'optparse'
3
3
 
4
4
  require 'tkellem'
5
- require 'tkellem/socket_server'
6
5
 
7
6
  class Tkellem::Daemon
8
7
  attr_reader :options
@@ -86,11 +85,8 @@ class Tkellem::Daemon
86
85
  end
87
86
 
88
87
  def start
89
- trap("INT") { EM.stop }
90
- EM.run do
91
- @admin = EM.start_unix_domain_server(socket_file, Tkellem::SocketServer)
92
- Tkellem::TkellemServer.new
93
- end
88
+ remove_files
89
+ Tkellem::TkellemServer.new(options).run
94
90
  ensure
95
91
  remove_files
96
92
  end
@@ -138,7 +134,7 @@ class Tkellem::Daemon
138
134
  end
139
135
 
140
136
  def remove_files
141
- FileUtils.rm(socket_file)
137
+ FileUtils.rm(socket_file) if File.exist?(socket_file)
142
138
  return unless @daemon
143
139
  pid = File.read(pid_file) if File.file?(pid_file)
144
140
  if pid.to_i == Process.pid
@@ -20,7 +20,7 @@ class IrcMessage < Struct.new(:prefix, :command, :args, :ctcp)
20
20
 
21
21
  msg = self.new(prefix, command, args)
22
22
 
23
- if args.last.try(:match, %r{#{"\x01"}([^ ]+)([^\1]*)#{"\x01"}})
23
+ if args.last && args.last.match(%r{#{"\x01"}([^ ]+)([^\1]*)#{"\x01"}})
24
24
  msg.ctcp = $1.upcase
25
25
  msg.args[-1] = $2.strip
26
26
  end
@@ -1,33 +1,50 @@
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
1
+ Sequel.migration do
2
+ up do
3
+ if self.table_exists?(:schema_migrations) && self[:schema_migrations].first(:version => '1')
4
+ next
7
5
  end
8
6
 
9
- create_table 'users' do |t|
10
- t.string 'username', :null => false
11
- t.string 'password'
12
- t.string 'role', :null => false, :default => 'user'
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
13
12
  end
14
13
 
15
- create_table 'networks' do |t|
16
- t.belongs_to 'user'
17
- t.string 'name', :null => false
14
+ create_table 'users' do
15
+ primary_key :id
16
+ String 'username', :null => false
17
+ String 'password'
18
+ String 'role', :null => false, :default => 'user'
18
19
  end
19
20
 
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
21
+ create_table 'networks' do
22
+ primary_key :id
23
+ foreign_key :user_id, :users
24
+ String 'name', :null => false
25
25
  end
26
26
 
27
- create_table 'network_users' do |t|
28
- t.belongs_to 'user'
29
- t.belongs_to 'network'
30
- t.string 'nick'
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
31
33
  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'
32
49
  end
33
50
  end
@@ -1,7 +1,24 @@
1
- class AtConnectColumns < ActiveRecord::Migration
2
- def self.up
3
- add_column 'networks', 'at_connect', 'text'
4
- add_column 'network_users', 'at_connect', 'text'
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
5
22
  end
6
23
  end
7
24
 
@@ -1,13 +1,22 @@
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
1
+ Sequel.migration do
2
+ up do
3
+ if self.table_exists?(:schema_migrations) && self[:schema_migrations].first(:version => '3')
4
+ next
7
5
  end
8
6
 
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')
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'
12
21
  end
13
22
  end
@@ -1,7 +1,7 @@
1
1
  module Tkellem
2
2
 
3
- class Host < ActiveRecord::Base
4
- belongs_to :network
3
+ class Host < Sequel::Model
4
+ many_to_one :network
5
5
 
6
6
  def to_s
7
7
  self.class.address_string(address, port, ssl)
@@ -1,12 +1,24 @@
1
1
  module Tkellem
2
2
 
3
- class ListenAddress < ActiveRecord::Base
3
+ class ListenAddress < Sequel::Model
4
+ plugin :validation_class_methods
5
+
4
6
  validates_uniqueness_of :port, :scope => [:address]
5
7
  validates_presence_of :address, :port
6
8
 
7
9
  def to_s
8
10
  "#{ssl ? 'ircs' : 'irc'}://#{address}:#{port}"
9
11
  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
10
22
  end
11
23
 
12
24
  end