hector 1.0.0
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.
- 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
|