hector 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ module Hector
2
+ class << self
3
+ def defer(&block)
4
+ EM.defer(&block)
5
+ end
6
+
7
+ def next_tick(&block)
8
+ EM.next_tick(&block)
9
+ end
10
+ end
11
+ 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,16 @@
1
+ module Hector
2
+ class NullLogger
3
+ def level=(l) end
4
+ def debug(*) end
5
+ def info(*) end
6
+ def warn(*) end
7
+ def error(*) end
8
+ def fatal(*) end
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :logger
13
+ end
14
+
15
+ self.logger = NullLogger.new
16
+ 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