tkellem 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +17 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/bin/tkellem +110 -0
- data/examples/config.yml +29 -0
- data/lib/tkellem/backlog.rb +85 -0
- data/lib/tkellem/bouncer.rb +52 -0
- data/lib/tkellem/bouncer_connection.rb +149 -0
- data/lib/tkellem/irc_line.rb +58 -0
- data/lib/tkellem/irc_server.rb +156 -0
- data/lib/tkellem.rb +45 -0
- data/spec/spec_helper.rb +10 -0
- metadata +89 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Brian Palmer <brian@codekitchen.net>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# tkellem
|
2
|
+
|
3
|
+
tkellem is an IRC bouncer, a proxy that keeps you permanently logged on to an
|
4
|
+
IRC server and stores all messages so that when your client next connects, you
|
5
|
+
can see the backlog of what happened while you were gone.
|
6
|
+
|
7
|
+
tkellem supports multiple device-independent backlogs, and connecting to
|
8
|
+
multiple IRC servers all from the same process.
|
9
|
+
|
10
|
+
## IMPORTANT
|
11
|
+
|
12
|
+
This is a very, very early version. Only the basic functionality is implemented.
|
13
|
+
You probably don't want this yet.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
bin/tkellem examples/config.yml
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rspec'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
9
|
+
spec.pattern = 'spec/spec_helper.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = 'tkellem'
|
16
|
+
gem.summary = 'IRC bouncer with multi-client support'
|
17
|
+
gem.email = 'brian@codekitchen.net'
|
18
|
+
gem.homepage = 'http://github.com/codekitchen/tkellem'
|
19
|
+
gem.authors = ['Brian Palmer']
|
20
|
+
gem.add_dependency 'eventmachine'
|
21
|
+
gem.executables = %w(tkellem)
|
22
|
+
end
|
23
|
+
|
24
|
+
Jeweler::GemcutterTasks.new
|
25
|
+
rescue LoadError
|
26
|
+
# om nom nom
|
27
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.7.0
|
data/bin/tkellem
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
require 'yaml'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
opts = OptionParser.new
|
11
|
+
opts.banner = <<BANNER
|
12
|
+
Usage: #{opts.program_name} PATH_TO_CONFIG
|
13
|
+
|
14
|
+
Start a tkellem instance using the specified configuration file (or ~/.tkellem/config.yml if none given)
|
15
|
+
BANNER
|
16
|
+
|
17
|
+
opts.on_tail("-h", "--help") { puts opts; exit }
|
18
|
+
|
19
|
+
rest = opts.parse(ARGV)
|
20
|
+
config_filename = rest.first || File.expand_path(ENV["HOME"]+'/.tkellem/config.yml')
|
21
|
+
|
22
|
+
config = YAML.load_file(config_filename)
|
23
|
+
|
24
|
+
$LOAD_PATH.push(File.expand_path(File.dirname(__FILE__)+'/../lib'))
|
25
|
+
require 'tkellem'
|
26
|
+
|
27
|
+
EM.run do
|
28
|
+
bouncer =
|
29
|
+
Tkellem::Bouncer.new(config['listen'] || '0.0.0.0',
|
30
|
+
config['port'] || 10001,
|
31
|
+
config['ssl'])
|
32
|
+
|
33
|
+
bouncer.max_backlog = config['max_backlog'].to_i
|
34
|
+
|
35
|
+
bouncer.on_authenticate do |username, password, irc_server|
|
36
|
+
server_config = config['connections'][irc_server.name]
|
37
|
+
if server_config && password_sha1 = server_config['password_sha1']
|
38
|
+
require 'openssl'
|
39
|
+
password_sha1 == OpenSSL::Digest::SHA1.hexdigest(password)
|
40
|
+
else
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_irc_server(bouncer, name, conn)
|
46
|
+
server = bouncer.add_irc_server(name,
|
47
|
+
conn['host'], conn['port'], conn['ssl'],
|
48
|
+
conn['nick'])
|
49
|
+
|
50
|
+
(conn['rooms'] || []).each { |room| server.join_room(room['name']) }
|
51
|
+
conn['clients'].each { |client| server.add_client(client['name']) }
|
52
|
+
end
|
53
|
+
|
54
|
+
config['connections'].each do |name, conn|
|
55
|
+
Tkellem::EasyLogger.logger.info("adding new connection #{name}")
|
56
|
+
add_irc_server(bouncer, name, conn)
|
57
|
+
end
|
58
|
+
|
59
|
+
Signal.trap('HUP') do
|
60
|
+
Tkellem::EasyLogger.logger.warn("got HUP, reloading #{config_filename}")
|
61
|
+
new_config = YAML.load_file(config_filename)
|
62
|
+
|
63
|
+
bouncer.max_backlog = new_config['max_backlog'].to_i
|
64
|
+
|
65
|
+
# find changed connections
|
66
|
+
to_delete = config['connections'].keys
|
67
|
+
|
68
|
+
new_config['connections'].each do |name, new_conn|
|
69
|
+
to_delete.delete(name)
|
70
|
+
conn = config['connections'][name]
|
71
|
+
if !conn
|
72
|
+
Tkellem::EasyLogger.logger.info("adding new connection #{name}")
|
73
|
+
add_irc_server(bouncer, name, new_conn)
|
74
|
+
else
|
75
|
+
if new_conn['host'] != conn['host'] || new_conn['port'] != conn['port'] || new_conn['ssl'] != conn['ssl']
|
76
|
+
Tkellem::EasyLogger.logger.info("server settings changed for #{name}, dropping clients and reconnecting")
|
77
|
+
bouncer.remove_irc_server(name)
|
78
|
+
add_irc_server(bouncer, name, new_conn)
|
79
|
+
elsif conn['clients'] != new_conn['clients']
|
80
|
+
irc_server = bouncer.get_irc_server(name)
|
81
|
+
|
82
|
+
# we don't have to reconnect, but maybe clients changed. we ignore
|
83
|
+
# changes to nick and rooms on HUP, since those are dynamic once tkellem
|
84
|
+
# connects. password change will be caught on the next client
|
85
|
+
# connect.
|
86
|
+
clients_to_delete = conn['clients'].map { |c| c['name'] }
|
87
|
+
new_conn['clients'].each do |new_client|
|
88
|
+
clients_to_delete.delete(new_client['name'])
|
89
|
+
# add -- if already exists, this is a no-op. this may change in the
|
90
|
+
# future though...
|
91
|
+
Tkellem::EasyLogger.logger.info("adding client #{new_client['name']} to server #{name}")
|
92
|
+
irc_server.add_client(new_client['name'])
|
93
|
+
end
|
94
|
+
clients_to_delete.each do |client_name|
|
95
|
+
Tkellem::EasyLogger.logger.info("removing client #{client_name} from server #{name}")
|
96
|
+
irc_server.remove_client(client_name) if irc_server
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
to_delete.each do |name|
|
103
|
+
Tkellem::EasyLogger.logger.info("deleting connection #{name}")
|
104
|
+
bouncer.remove_irc_server(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
config = new_config
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
data/examples/config.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# $ bin/tkellem examples/config.yml
|
2
|
+
#
|
3
|
+
# to connect: connect to localhost port 10001, ssl enabled, "real name" set to
|
4
|
+
#
|
5
|
+
# <connection_name> <client_name>
|
6
|
+
#
|
7
|
+
# e.g.:
|
8
|
+
#
|
9
|
+
# freenode laptop
|
10
|
+
---
|
11
|
+
listen: 0.0.0.0
|
12
|
+
port: 10001
|
13
|
+
ssl: true
|
14
|
+
# max_backlog: 500
|
15
|
+
connections:
|
16
|
+
freenode:
|
17
|
+
host: irc.freenode.org
|
18
|
+
port: 6667
|
19
|
+
ssl: false
|
20
|
+
nick: tkellem_r0ck
|
21
|
+
# Uncomment to enable password auth (there's no auth by default, which
|
22
|
+
# means anybody can connect). You can generate a password_sha1 like so:
|
23
|
+
# echo -n 'tkellem_r0ck' | openssl sha1
|
24
|
+
# password_sha1: a4f4a3b97c7b8a028d4e3f3fee85d6e5626baba5
|
25
|
+
rooms:
|
26
|
+
- name: "#tkellem_test"
|
27
|
+
clients:
|
28
|
+
- name: laptop
|
29
|
+
- name: iphone
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'tkellem/irc_line'
|
2
|
+
|
3
|
+
module Tkellem
|
4
|
+
|
5
|
+
# Normally there will be one client per backlog, but there can be more than one
|
6
|
+
# connection for the same backlog, if two or more IRC clients connect with the
|
7
|
+
# same client name. That situation is equivalent to how most multi-connection
|
8
|
+
# bouncers like bip work.
|
9
|
+
|
10
|
+
class Backlog
|
11
|
+
|
12
|
+
class BacklogLine < Struct.new(:irc_line, :time)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(name, max_backlog = nil)
|
16
|
+
@name = name
|
17
|
+
@backlog = []
|
18
|
+
@pm_backlogs = Hash.new { |h,k| h[k] = [] }
|
19
|
+
@active_conns = []
|
20
|
+
@max_backlog = max_backlog
|
21
|
+
end
|
22
|
+
attr_reader :name, :backlog, :active_conns, :pm_backlogs, :max_backlog
|
23
|
+
|
24
|
+
def handle_message(msg)
|
25
|
+
# TODO: only send back response messages like WHO, NAMES, etc. to the
|
26
|
+
# BouncerConnection that requested it.
|
27
|
+
if !active_conns.empty?
|
28
|
+
case msg.command
|
29
|
+
when /3\d\d/, /join/i, /part/i
|
30
|
+
# transient response -- we want to forward these, but not backlog
|
31
|
+
active_conns.each { |conn| conn.transient_response(msg) }
|
32
|
+
when /privmsg/i
|
33
|
+
active_conns.each { |conn| conn.send_msg(msg) }
|
34
|
+
else
|
35
|
+
# do nothing?
|
36
|
+
end
|
37
|
+
elsif msg.command.match(/privmsg/i)
|
38
|
+
if msg.args.first.match(/^#/)
|
39
|
+
# room privmsg always goes in a specific backlog
|
40
|
+
pm_target = msg.args.first
|
41
|
+
bl = pm_backlogs[pm_target]
|
42
|
+
else
|
43
|
+
# other messages go in the general backlog
|
44
|
+
bl = backlog
|
45
|
+
end
|
46
|
+
bl.push(BacklogLine.new(msg, Time.now))
|
47
|
+
limit_backlog(bl)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def limit_backlog(bl)
|
52
|
+
bl.shift until !max_backlog || bl.size <= max_backlog
|
53
|
+
end
|
54
|
+
|
55
|
+
def max_backlog=(new_val)
|
56
|
+
@max_backlog = new_val
|
57
|
+
limit_backlog(backlog)
|
58
|
+
pm_backlogs.each { |k,bl| limit_backlog(bl) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_conn(bouncer_conn)
|
62
|
+
active_conns << bouncer_conn
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_conn(bouncer_conn)
|
66
|
+
active_conns.delete(bouncer_conn)
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_backlog(conn, pm_target = nil)
|
70
|
+
if pm_target
|
71
|
+
# send room-specific backlog
|
72
|
+
msgs = pm_backlogs.key?(pm_target) ? pm_backlogs[pm_target] : []
|
73
|
+
else
|
74
|
+
# send the general backlog
|
75
|
+
msgs = backlog
|
76
|
+
end
|
77
|
+
|
78
|
+
until msgs.empty?
|
79
|
+
backlog_line = msgs.shift
|
80
|
+
conn.send_msg(backlog_line.irc_line.with_timestamp(backlog_line.time))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'tkellem/irc_server'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
class Bouncer
|
7
|
+
def initialize(listen_address, port, do_ssl)
|
8
|
+
@irc_servers = {}
|
9
|
+
@max_backlog = nil
|
10
|
+
@server = EM.start_server(listen_address, port, BouncerConnection,
|
11
|
+
self, do_ssl)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_irc_server(name, host, port, do_ssl, nick)
|
15
|
+
server = EM.connect(host, port, IrcServer, self, name, do_ssl, nick)
|
16
|
+
@irc_servers[name] = server
|
17
|
+
server.set_max_backlog(@max_backlog) if @max_backlog
|
18
|
+
server
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove_irc_server(name)
|
22
|
+
server = @irc_servers.delete(name)
|
23
|
+
if server
|
24
|
+
server.close_connection(true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_authenticate(&block)
|
29
|
+
@auth_block = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_backlog=(max_backlog)
|
33
|
+
@max_backlog = max_backlog && max_backlog > 0 ? max_backlog : nil
|
34
|
+
@irc_servers.each { |name, server| server.set_max_backlog(@max_backlog) }
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Internal API
|
39
|
+
|
40
|
+
def get_irc_server(name) #:nodoc:
|
41
|
+
@irc_servers[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def do_auth(username, password, irc_server) #:nodoc:
|
45
|
+
if @auth_block
|
46
|
+
@auth_block.call(username, password.to_s, irc_server)
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'tkellem/irc_line'
|
3
|
+
require 'tkellem/backlog'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
|
7
|
+
module BouncerConnection
|
8
|
+
include EM::Protocols::LineText2
|
9
|
+
include Tkellem::EasyLogger
|
10
|
+
|
11
|
+
def initialize(bouncer, do_ssl)
|
12
|
+
set_delimiter "\r\n"
|
13
|
+
|
14
|
+
@ssl = do_ssl
|
15
|
+
@bouncer = bouncer
|
16
|
+
|
17
|
+
@irc_server = nil
|
18
|
+
@backlog = nil
|
19
|
+
@nick = nil
|
20
|
+
@conn_name = nil
|
21
|
+
@name = nil
|
22
|
+
end
|
23
|
+
attr_reader :ssl, :irc_server, :backlog, :bouncer, :nick
|
24
|
+
|
25
|
+
def connected?
|
26
|
+
!!irc_server
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
@name || "new-conn"
|
31
|
+
end
|
32
|
+
|
33
|
+
def post_init
|
34
|
+
if ssl
|
35
|
+
debug "starting TLS"
|
36
|
+
start_tls :verify_peer => false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ssl_handshake_completed
|
41
|
+
debug "TLS complete"
|
42
|
+
end
|
43
|
+
|
44
|
+
def error!(msg)
|
45
|
+
info("ERROR :#{msg}")
|
46
|
+
send_msg("ERROR :#{msg}")
|
47
|
+
close_connection(true)
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect(conn_name, client_name, password)
|
51
|
+
@irc_server = bouncer.get_irc_server(conn_name.downcase)
|
52
|
+
unless irc_server && irc_server.connected?
|
53
|
+
error!("unknown connection #{conn_name}")
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
unless bouncer.do_auth(conn_name, @password, irc_server)
|
58
|
+
error!("bad auth, please check your password")
|
59
|
+
@irc_server = @conn_name = @name = @backlog = nil
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
@conn_name = conn_name
|
64
|
+
@name = client_name
|
65
|
+
@backlog = irc_server.bouncer_connect(self)
|
66
|
+
unless backlog
|
67
|
+
error!("unknown client #{client_name}")
|
68
|
+
@irc_server = @conn_name = @name = nil
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
info "connected"
|
73
|
+
|
74
|
+
irc_server.send_welcome(self)
|
75
|
+
backlog.send_backlog(self)
|
76
|
+
irc_server.rooms.each { |room| simulate_join(room) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def tkellem(msg)
|
80
|
+
case msg.args.first
|
81
|
+
when /nothing_yet/i
|
82
|
+
else
|
83
|
+
send_msg(":tkellem!tkellem@tkellem PRIVMSG #{nick} :Unknown tkellem command #{msg.args.first}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_line(line)
|
88
|
+
trace "from client: #{line}"
|
89
|
+
msg = IrcLine.parse(line)
|
90
|
+
case msg.command
|
91
|
+
when /tkellem/i
|
92
|
+
tkellem(msg)
|
93
|
+
when /pass/i
|
94
|
+
@password = msg.args.first
|
95
|
+
when /user/i
|
96
|
+
conn_name, client_name = msg.args.last.strip.split(' ')
|
97
|
+
connect(conn_name, client_name, @password)
|
98
|
+
when /nick/i
|
99
|
+
if connected?
|
100
|
+
irc_server.change_nick(msg.last)
|
101
|
+
else
|
102
|
+
@nick = msg.last
|
103
|
+
end
|
104
|
+
when /quit/i
|
105
|
+
# DENIED
|
106
|
+
close_connection
|
107
|
+
when /ping/i
|
108
|
+
send_msg(":tkellem PONG tkellem :#{msg.last}")
|
109
|
+
else
|
110
|
+
if !connected?
|
111
|
+
close_connection
|
112
|
+
else
|
113
|
+
# pay it forward
|
114
|
+
irc_server.send_msg(msg)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def simulate_join(room)
|
120
|
+
send_msg(":#{irc_server.nick}!#{name}@tkellem JOIN #{room}")
|
121
|
+
# TODO: intercept the NAMES response so that only this bouncer gets it
|
122
|
+
# Otherwise other clients might show an "in this room" line.
|
123
|
+
irc_server.send_msg("NAMES #{room}\r\n")
|
124
|
+
end
|
125
|
+
|
126
|
+
def transient_response(msg)
|
127
|
+
send_msg(msg)
|
128
|
+
if msg.command == "366"
|
129
|
+
# finished joining this room, let's backlog it
|
130
|
+
debug "got final NAMES for #{msg.args[1]}, sending backlog"
|
131
|
+
backlog.send_backlog(self, msg.args[1])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def send_msg(msg)
|
136
|
+
trace "to client: #{msg}"
|
137
|
+
send_data("#{msg}\r\n")
|
138
|
+
end
|
139
|
+
|
140
|
+
def log_name
|
141
|
+
"#{@conn_name}-#{name}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def unbind
|
145
|
+
irc_server.bouncer_disconnect(self) if connected?
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tkellem
|
2
|
+
|
3
|
+
class IrcLine
|
4
|
+
RE = %r{(:[^ ]+ )?([^ ]*)(.*)}i
|
5
|
+
|
6
|
+
def self.parse(line)
|
7
|
+
md = RE.match(line) or raise("invalid input: #{line.inspect}")
|
8
|
+
|
9
|
+
self.new(line, md[1], md[2], md[3])
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :prefix, :command, :args
|
13
|
+
|
14
|
+
def initialize(orig, prefix, command, args)
|
15
|
+
@orig = orig
|
16
|
+
@prefix = prefix ? prefix.strip : nil
|
17
|
+
@command = command
|
18
|
+
|
19
|
+
args.strip!
|
20
|
+
idx = args.index(":")
|
21
|
+
if idx
|
22
|
+
@args = args[0...idx].split(' ') + [args[idx+1..-1]]
|
23
|
+
else
|
24
|
+
@args = args.split(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def command?(cmd)
|
29
|
+
@command.downcase == cmd.downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
def replay
|
33
|
+
@orig
|
34
|
+
end
|
35
|
+
alias_method :to_s, :replay
|
36
|
+
|
37
|
+
def last
|
38
|
+
args.last
|
39
|
+
end
|
40
|
+
|
41
|
+
def target_user
|
42
|
+
if prefix && md = %r{^:([^!]+)}.match(prefix)
|
43
|
+
md[1]
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_timestamp(timestamp)
|
50
|
+
new_command = [prefix, command]
|
51
|
+
new_command += args[0..-2]
|
52
|
+
new_command.push("#{timestamp.strftime("%H:%M:%S")}> #{args.last}")
|
53
|
+
new_command.join(' ')
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'tkellem/irc_line'
|
4
|
+
require 'tkellem/bouncer_connection'
|
5
|
+
require 'tkellem/backlog'
|
6
|
+
|
7
|
+
module Tkellem
|
8
|
+
|
9
|
+
module IrcServer
|
10
|
+
include EM::Protocols::LineText2
|
11
|
+
include Tkellem::EasyLogger
|
12
|
+
|
13
|
+
def initialize(bouncer, name, do_ssl, nick)
|
14
|
+
set_delimiter "\r\n"
|
15
|
+
|
16
|
+
@bouncer = bouncer
|
17
|
+
@name = name
|
18
|
+
@ssl = do_ssl
|
19
|
+
@nick = nick
|
20
|
+
|
21
|
+
@max_backlog = nil
|
22
|
+
@connected = false
|
23
|
+
@welcomes = []
|
24
|
+
@rooms = Set.new
|
25
|
+
@backlogs = {}
|
26
|
+
@active_conns = []
|
27
|
+
@joined_rooms = false
|
28
|
+
@pending_rooms = []
|
29
|
+
end
|
30
|
+
attr_reader :name, :backlogs, :welcomes, :rooms, :nick, :active_conns
|
31
|
+
alias_method :log_name, :name
|
32
|
+
|
33
|
+
def connected?
|
34
|
+
@connected
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_max_backlog(max_backlog)
|
38
|
+
@max_backlog = max_backlog
|
39
|
+
backlogs.each { |name, backlog| backlog.max_backlog = max_backlog }
|
40
|
+
end
|
41
|
+
|
42
|
+
def post_init
|
43
|
+
if @ssl
|
44
|
+
debug "starting TLS"
|
45
|
+
# TODO: support strict cert checks
|
46
|
+
start_tls :verify_peer => false
|
47
|
+
else
|
48
|
+
ssl_handshake_completed
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ssl_handshake_completed
|
53
|
+
# TODO: support sending a real username, realname, etc
|
54
|
+
send_msg("USER #{nick} localhost blah :#{nick}")
|
55
|
+
change_nick(nick, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def receive_line(line)
|
59
|
+
trace "from server: #{line}"
|
60
|
+
msg = IrcLine.parse(line)
|
61
|
+
|
62
|
+
case msg.command
|
63
|
+
when /0\d\d/, /2[56]\d/, /37[256]/
|
64
|
+
welcomes << msg
|
65
|
+
got_welcome if msg.command == "376" # end of MOTD
|
66
|
+
when /join/i
|
67
|
+
debug "#{msg.target_user} joined #{msg.last}"
|
68
|
+
rooms << msg.last if msg.target_user == nick
|
69
|
+
when /part/i
|
70
|
+
debug "#{msg.target_user} left #{msg.last}"
|
71
|
+
rooms.delete(msg.last) if msg.target_user == nick
|
72
|
+
when /ping/i
|
73
|
+
send_msg("PONG #{nick}!tkellem #{msg.args.first}")
|
74
|
+
when /pong/i
|
75
|
+
# swallow it, we handle ping-pong from clients separately, in
|
76
|
+
# BouncerConnection
|
77
|
+
else
|
78
|
+
end
|
79
|
+
|
80
|
+
backlogs.each { |name, backlog| backlog.handle_message(msg) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def got_welcome
|
84
|
+
return if @joined_rooms
|
85
|
+
@joined_rooms = true
|
86
|
+
@pending_rooms.each do |room|
|
87
|
+
join_room(room)
|
88
|
+
end
|
89
|
+
@pending_rooms.clear
|
90
|
+
|
91
|
+
# We're all initialized, allow connections
|
92
|
+
@connected = true
|
93
|
+
end
|
94
|
+
|
95
|
+
def change_nick(new_nick, force = false)
|
96
|
+
return if !force && new_nick == @nick
|
97
|
+
@nick = new_nick
|
98
|
+
send_msg("NICK #{new_nick}")
|
99
|
+
end
|
100
|
+
|
101
|
+
def join_room(room_name)
|
102
|
+
if @joined_rooms
|
103
|
+
send_msg("JOIN #{room_name}")
|
104
|
+
else
|
105
|
+
@pending_rooms << room_name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_client(name)
|
110
|
+
return if backlogs[name]
|
111
|
+
backlog = Backlog.new(name, @max_backlog)
|
112
|
+
backlogs[name] = backlog
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_client(name)
|
116
|
+
backlog = backlogs.delete(name)
|
117
|
+
if backlog
|
118
|
+
backlog.active_conns.each do |conn|
|
119
|
+
conn.error!("client removed")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def send_msg(msg)
|
125
|
+
trace "to server: #{msg}"
|
126
|
+
send_data("#{msg}\r\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
def send_welcome(bouncer_conn)
|
130
|
+
welcomes.each { |msg| bouncer_conn.send_msg(msg) }
|
131
|
+
end
|
132
|
+
|
133
|
+
def unbind
|
134
|
+
debug "OMG we got disconnected."
|
135
|
+
# TODO: reconnect if desired. but not if this server was explicitly shut
|
136
|
+
# down or removed.
|
137
|
+
backlogs.keys.each { |name| remove_client(name) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def bouncer_connect(bouncer_conn)
|
141
|
+
return nil unless backlogs[bouncer_conn.name]
|
142
|
+
|
143
|
+
active_conns << bouncer_conn
|
144
|
+
backlogs[bouncer_conn.name].add_conn(bouncer_conn)
|
145
|
+
backlogs[bouncer_conn.name]
|
146
|
+
end
|
147
|
+
|
148
|
+
def bouncer_disconnect(bouncer_conn)
|
149
|
+
return nil unless backlogs[bouncer_conn.name]
|
150
|
+
|
151
|
+
backlogs[bouncer_conn.name].remove_conn(bouncer_conn)
|
152
|
+
active_conns.delete(bouncer_conn)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
data/lib/tkellem.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
pathname = File.expand_path(File.dirname(__FILE__))
|
7
|
+
$LOAD_PATH.push(pathname) unless $LOAD_PATH.include?(pathname)
|
8
|
+
|
9
|
+
module Tkellem
|
10
|
+
module EasyLogger
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
def self.logger=(new_logger)
|
14
|
+
@logger = new_logger
|
15
|
+
end
|
16
|
+
def self.logger
|
17
|
+
return @logger if @logger
|
18
|
+
@logger = Logger.new(STDERR)
|
19
|
+
@logger.datetime_format = "%Y-%m-%d"
|
20
|
+
@logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.trace=(val)
|
24
|
+
@trace = val
|
25
|
+
end
|
26
|
+
def self.trace
|
27
|
+
@trace || @trace = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def trace(msg)
|
31
|
+
puts("TRACE: #{log_name}: #{msg}") if EasyLogger.trace
|
32
|
+
end
|
33
|
+
|
34
|
+
::Logger::Severity.constants.each do |level|
|
35
|
+
next if level == "UNKNOWN"
|
36
|
+
module_eval(<<-EVAL, __FILE__, __LINE__)
|
37
|
+
def #{level.downcase}(msg)
|
38
|
+
EasyLogger.logger.#{level.downcase}("\#{log_name} (\#{object_id}): \#{msg}")
|
39
|
+
end
|
40
|
+
EVAL
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require 'tkellem/bouncer'
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tkellem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 7
|
8
|
+
- 0
|
9
|
+
version: 0.7.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Brian Palmer
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-17 00:00:00 -07:00
|
18
|
+
default_executable: tkellem
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description:
|
34
|
+
email: brian@codekitchen.net
|
35
|
+
executables:
|
36
|
+
- tkellem
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
files:
|
43
|
+
- LICENSE
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- VERSION
|
47
|
+
- bin/tkellem
|
48
|
+
- examples/config.yml
|
49
|
+
- lib/tkellem.rb
|
50
|
+
- lib/tkellem/backlog.rb
|
51
|
+
- lib/tkellem/bouncer.rb
|
52
|
+
- lib/tkellem/bouncer_connection.rb
|
53
|
+
- lib/tkellem/irc_line.rb
|
54
|
+
- lib/tkellem/irc_server.rb
|
55
|
+
- spec/spec_helper.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/codekitchen/tkellem
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.7
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: IRC bouncer with multi-client support
|
88
|
+
test_files:
|
89
|
+
- spec/spec_helper.rb
|