hector 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/hector.rb +42 -0
- data/lib/hector/boot.rb +62 -0
- data/lib/hector/channel.rb +106 -0
- data/lib/hector/commands/away.rb +16 -0
- data/lib/hector/commands/join.rb +16 -0
- data/lib/hector/commands/mode.rb +31 -0
- data/lib/hector/commands/names.rb +16 -0
- data/lib/hector/commands/nick.rb +11 -0
- data/lib/hector/commands/notice.rb +10 -0
- data/lib/hector/commands/part.rb +11 -0
- data/lib/hector/commands/ping.rb +9 -0
- data/lib/hector/commands/pong.rb +9 -0
- data/lib/hector/commands/privmsg.rb +16 -0
- data/lib/hector/commands/quit.rb +10 -0
- data/lib/hector/commands/realname.rb +10 -0
- data/lib/hector/commands/topic.rb +26 -0
- data/lib/hector/commands/who.rb +29 -0
- data/lib/hector/commands/whois.rb +28 -0
- data/lib/hector/concerns/authentication.rb +45 -0
- data/lib/hector/concerns/keep_alive.rb +24 -0
- data/lib/hector/concerns/presence.rb +59 -0
- data/lib/hector/connection.rb +85 -0
- data/lib/hector/deference.rb +11 -0
- data/lib/hector/errors.rb +30 -0
- data/lib/hector/heartbeat.rb +25 -0
- data/lib/hector/identity.rb +23 -0
- data/lib/hector/logging.rb +16 -0
- data/lib/hector/request.rb +42 -0
- data/lib/hector/response.rb +48 -0
- data/lib/hector/server.rb +14 -0
- data/lib/hector/service.rb +30 -0
- data/lib/hector/session.rb +185 -0
- data/lib/hector/user_session.rb +38 -0
- data/lib/hector/version.rb +3 -0
- data/lib/hector/yaml_identity_adapter.rb +56 -0
- metadata +133 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Hector
|
2
|
+
module Commands
|
3
|
+
module Whois
|
4
|
+
def on_whois
|
5
|
+
nickname = request.args.first
|
6
|
+
if session = Session.find(nickname)
|
7
|
+
respond_to_whois_for(self.nickname, session)
|
8
|
+
else
|
9
|
+
raise NoSuchNickOrChannel, nickname
|
10
|
+
end
|
11
|
+
ensure
|
12
|
+
respond_with("318", self.nickname, nickname, "End of /WHOIS list.")
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to_whois_for(destination, session)
|
16
|
+
respond_with("301", session.nickname, :text => session.away_message) if session.away?
|
17
|
+
respond_with("311", destination, session.nickname, session.whois)
|
18
|
+
respond_with("319", destination, session.nickname, :text => channels.map { |channel| channel.name }.join(" ")) unless channels.empty?
|
19
|
+
respond_with("312", destination, session.nickname, Hector.server_name, :text => "Hector")
|
20
|
+
respond_with("317", destination, session.nickname, session.seconds_idle, session.created_at, :text => "seconds idle, signon time")
|
21
|
+
end
|
22
|
+
|
23
|
+
def whois
|
24
|
+
"#{nickname} #{identity.username} #{Hector.server_name} * :#{realname}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Hector
|
2
|
+
module Concerns
|
3
|
+
module Authentication
|
4
|
+
def on_user
|
5
|
+
@username = request.args.first
|
6
|
+
@realname = request.text
|
7
|
+
authenticate
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_pass
|
11
|
+
@password = request.text
|
12
|
+
authenticate
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_nick
|
16
|
+
@nickname = request.text
|
17
|
+
authenticate
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def authenticate
|
22
|
+
set_identity
|
23
|
+
set_session
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_identity
|
27
|
+
if @username && @password && !@identity
|
28
|
+
Identity.authenticate(@username, @password) do |identity|
|
29
|
+
if @identity = identity
|
30
|
+
set_session
|
31
|
+
else
|
32
|
+
error InvalidPassword
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_session
|
39
|
+
if @identity && @nickname
|
40
|
+
@session = UserSession.create(@nickname, self, @identity, @realname)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hector
|
2
|
+
module Concerns
|
3
|
+
module KeepAlive
|
4
|
+
def initialize_keep_alive
|
5
|
+
@received_pong = true
|
6
|
+
@heartbeat = Hector::Heartbeat.new { on_heartbeat }
|
7
|
+
end
|
8
|
+
|
9
|
+
def on_heartbeat
|
10
|
+
if @received_pong
|
11
|
+
@received_pong = false
|
12
|
+
respond_with(:ping, Hector.server_name)
|
13
|
+
else
|
14
|
+
@quit_message = "Ping timeout"
|
15
|
+
connection.close_connection(true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy_keep_alive
|
20
|
+
@heartbeat.stop
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Hector
|
2
|
+
module Concerns
|
3
|
+
module Presence
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
attr_reader :created_at, :updated_at
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def channels
|
11
|
+
Channel.find_all_for_session(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize_presence
|
15
|
+
@created_at = Time.now
|
16
|
+
@updated_at = Time.now
|
17
|
+
deliver_welcome_message
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy_presence
|
21
|
+
deliver_quit_message
|
22
|
+
leave_all_channels
|
23
|
+
end
|
24
|
+
|
25
|
+
def seconds_idle
|
26
|
+
Time.now - updated_at
|
27
|
+
end
|
28
|
+
|
29
|
+
def peer_sessions
|
30
|
+
[self, *channels.map { |channel| channel.sessions }.flatten].uniq
|
31
|
+
end
|
32
|
+
|
33
|
+
def touch_presence
|
34
|
+
@updated_at = Time.now
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
def deliver_welcome_message
|
39
|
+
respond_with("001", nickname, :text => "Welcome to IRC")
|
40
|
+
respond_with("422", :text => "MOTD File is missing")
|
41
|
+
end
|
42
|
+
|
43
|
+
def deliver_quit_message
|
44
|
+
broadcast(:quit, :source => source, :text => quit_message, :except => self)
|
45
|
+
respond_with(:error, :text => "Closing Link: #{nickname}[hector] (#{quit_message})")
|
46
|
+
end
|
47
|
+
|
48
|
+
def leave_all_channels
|
49
|
+
channels.each do |channel|
|
50
|
+
channel.part(self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def quit_message
|
55
|
+
@quit_message || "Connection closed"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Hector
|
2
|
+
class Connection < EventMachine::Protocols::LineAndTextProtocol
|
3
|
+
include Concerns::Authentication
|
4
|
+
|
5
|
+
attr_reader :session, :request, :identity
|
6
|
+
|
7
|
+
def post_init
|
8
|
+
log(:info, "opened connection")
|
9
|
+
end
|
10
|
+
|
11
|
+
def receive_line(line)
|
12
|
+
@request = Request.new(line)
|
13
|
+
log(:debug, "received", @request.to_s.inspect) unless @request.sensitive?
|
14
|
+
|
15
|
+
if session
|
16
|
+
session.receive(request)
|
17
|
+
else
|
18
|
+
if respond_to?(request.event_name)
|
19
|
+
send(request.event_name)
|
20
|
+
else
|
21
|
+
close_connection(true)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
rescue IrcError => e
|
26
|
+
handle_error(e)
|
27
|
+
|
28
|
+
rescue Exception => e
|
29
|
+
log(:error, [e, *e.backtrace].join("\n"))
|
30
|
+
|
31
|
+
ensure
|
32
|
+
@request = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def unbind
|
36
|
+
session.destroy if session
|
37
|
+
log(:info, "closing connection")
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_with(response, *args)
|
41
|
+
response = Response.new(response, *args) unless response.is_a?(Response)
|
42
|
+
send_data(response.to_s)
|
43
|
+
log(:debug, "sent", response.to_s.inspect)
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_error(error)
|
47
|
+
respond_with(error.response)
|
48
|
+
close_connection(true) if error.fatal?
|
49
|
+
end
|
50
|
+
|
51
|
+
def error(klass, *args)
|
52
|
+
handle_error(klass.new(*args))
|
53
|
+
end
|
54
|
+
|
55
|
+
def log(level, *args)
|
56
|
+
Hector.logger.send(level, [log_tag, *args].join(" "))
|
57
|
+
end
|
58
|
+
|
59
|
+
def address
|
60
|
+
peer_info[1]
|
61
|
+
end
|
62
|
+
|
63
|
+
def port
|
64
|
+
peer_info[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
def peer_info
|
69
|
+
@peer_info ||= Socket.unpack_sockaddr_in(get_peername)
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_tag
|
73
|
+
"[#{address}:#{port}]".tap do |tag|
|
74
|
+
tag << " (#{session.nickname})" if session
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class SSLConnection < Connection
|
80
|
+
def post_init
|
81
|
+
log(:info, "opened SSL connection")
|
82
|
+
start_tls
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hector
|
2
|
+
class Error < ::StandardError; end
|
3
|
+
class LoadError < Error; end
|
4
|
+
|
5
|
+
class IrcError < Error
|
6
|
+
def response
|
7
|
+
Response.new(command, *options).tap do |response|
|
8
|
+
response.args.push(message) unless message == self.class.name
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.IrcError(command, *options)
|
14
|
+
fatal = options.last.is_a?(Hash) && options.last.delete(:fatal)
|
15
|
+
Class.new(IrcError).tap do |klass|
|
16
|
+
klass.class_eval do
|
17
|
+
define_method(:command) { command.dup }
|
18
|
+
define_method(:options) { options.dup }
|
19
|
+
define_method(:fatal?) { fatal }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class NoSuchNickOrChannel < IrcError("401", :text => "No such nick/channel"); end
|
25
|
+
class NoSuchChannel < IrcError("403", :text => "No such channel"); end
|
26
|
+
class CannotSendToChannel < IrcError("404", :text => "Cannot send to channel"); end
|
27
|
+
class ErroneousNickname < IrcError("432", :text => "Erroneous nickname"); end
|
28
|
+
class NicknameInUse < IrcError("433", "*", :text => "Nickname is already in use"); end
|
29
|
+
class InvalidPassword < IrcError("464", :text => "Invalid password", :fatal => true); end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hector
|
2
|
+
class Heartbeat
|
3
|
+
def self.create_timer(interval, &block)
|
4
|
+
EventMachine::PeriodicTimer.new(interval, &block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(interval = 60, &block)
|
8
|
+
@interval, @block = interval, block
|
9
|
+
start
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
@timer ||= self.class.create_timer(@interval) { pulse }
|
14
|
+
end
|
15
|
+
|
16
|
+
def pulse
|
17
|
+
@block.call
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
@timer.cancel
|
22
|
+
@timer = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hector
|
2
|
+
class Identity
|
3
|
+
attr_accessor :username
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :adapter
|
7
|
+
|
8
|
+
def authenticate(username, password)
|
9
|
+
adapter.authenticate(username, password) do |authenticated|
|
10
|
+
yield authenticated ? new(username) : nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(username)
|
16
|
+
@username = username
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(identity)
|
20
|
+
Identity.adapter.normalize(username) == Identity.adapter.normalize(identity.username)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Hector
|
2
|
+
class Request
|
3
|
+
attr_reader :line, :command, :args, :text
|
4
|
+
alias_method :to_s, :line
|
5
|
+
|
6
|
+
def initialize(line)
|
7
|
+
@line = line
|
8
|
+
parse
|
9
|
+
end
|
10
|
+
|
11
|
+
def event_name
|
12
|
+
"on_#{command.downcase}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def sensitive?
|
16
|
+
command.downcase == "pass"
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def parse
|
21
|
+
source = line.dup
|
22
|
+
@command = extract!(source, /^ *([^ ]+)/, "").upcase
|
23
|
+
@text = extract!(source, / :(.*)$/)
|
24
|
+
@args = source.strip.split(" ")
|
25
|
+
|
26
|
+
if @text
|
27
|
+
@args.push(@text)
|
28
|
+
else
|
29
|
+
@text = @args.last
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract!(line, regex, default = nil)
|
34
|
+
result = nil
|
35
|
+
line.gsub!(regex) do |match|
|
36
|
+
result = $~[1]
|
37
|
+
""
|
38
|
+
end
|
39
|
+
result || default
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Hector
|
2
|
+
class Response
|
3
|
+
attr_reader :command, :args, :source
|
4
|
+
attr_accessor :text
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def apportion_text(args, *base_args)
|
8
|
+
base_response = Response.new(*base_args)
|
9
|
+
max_length = 510 - base_response.to_s.length
|
10
|
+
|
11
|
+
args.inject([args.shift.dup]) do |texts, arg|
|
12
|
+
if texts.last.length + arg.length + 1 >= max_length
|
13
|
+
texts << arg.dup
|
14
|
+
else
|
15
|
+
texts.last << " " << arg
|
16
|
+
end
|
17
|
+
texts
|
18
|
+
end.map do |text|
|
19
|
+
base_response.dup.tap do |response|
|
20
|
+
response.text = text
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(command, *args)
|
27
|
+
@command = command.to_s.upcase
|
28
|
+
@args = args
|
29
|
+
|
30
|
+
options = args.pop if args.last.is_a?(Hash)
|
31
|
+
@text = options[:text] if options
|
32
|
+
@source = options[:source] if options
|
33
|
+
end
|
34
|
+
|
35
|
+
def event_name
|
36
|
+
"received_#{command.downcase}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
[].tap do |line|
|
41
|
+
line.push(":#{source}") if source
|
42
|
+
line.push(command)
|
43
|
+
line.concat(args)
|
44
|
+
line.push(":#{text}") if text
|
45
|
+
end.join(" ")[0, 510] + "\r\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|