blur 1.8.6 → 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Blur
4
4
  class Network
@@ -13,14 +13,23 @@ module Blur
13
13
  class Connection < EM::Protocols::LineAndTextProtocol
14
14
  SSLValidationError = Class.new StandardError
15
15
 
16
+ # @return [Float] the default connection timeout interval in seconds.
17
+ DEFAULT_CONNECT_TIMEOUT_INTERVAL = 30
18
+
16
19
  # Check whether or not connection is established.
17
- def established?; @connected == true end
20
+ def established?
21
+ @connected == true
22
+ end
18
23
 
19
24
  # EventMachine instantiates this class, and then sends event messages to
20
25
  # that instance.
21
26
  def initialize network
22
27
  @network = network
23
28
  @connected = false
29
+ connect_timeout = network.options.fetch 'connect_timeout',
30
+ DEFAULT_CONNECT_TIMEOUT_INTERVAL
31
+
32
+ self.pending_connect_timeout = connect_timeout
24
33
 
25
34
  super
26
35
  end
@@ -28,19 +37,18 @@ module Blur
28
37
  # Called when a new connection is being set up, all we're going to use
29
38
  # it for is to enable SSL/TLS on our connection.
30
39
  def post_init
31
- if @network.secure?
32
- verify_peer = (@network.options[:ssl_no_verify] ? false : true)
40
+ return unless @network.secure?
33
41
 
34
- start_tls verify_peer: verify_peer
35
- end
42
+ verify_peer = (@network.options[:ssl_no_verify] ? false : true)
43
+ start_tls verify_peer: verify_peer
36
44
  end
37
45
 
38
46
  # Called when a line was received, the connection sends it to the network
39
47
  # delegate which then sends it to the client.
40
48
  def receive_line line
41
- command = Command.parse line
49
+ message = IRCParser::Message.parse line
42
50
 
43
- @network.got_command command
51
+ @network.got_message message
44
52
  end
45
53
 
46
54
  # Called when the SSL handshake was completed with the remote server,
@@ -61,12 +69,8 @@ module Blur
61
69
  ssl_cert_file = @network.options[:ssl_cert_file]
62
70
  peer_certificate = OpenSSL::X509::Certificate.new peer_cert
63
71
 
64
- if ssl_cert_file
65
- unless File.readable? ssl_cert_file
66
- raise SSLValidationError, "Could not read the CA certificate file."
67
-
68
- return false
69
- end
72
+ if ssl_cert_file && !File.readable?(ssl_cert_file)
73
+ raise SSLValidationError, 'Could not read the CA certificate file.'
70
74
  end
71
75
 
72
76
  if fingerprint_verification?
@@ -75,9 +79,7 @@ module Blur
75
79
 
76
80
  if fingerprint != peer_fingerprint
77
81
  raise SSLValidationError,
78
- "Expected fingerprint '#{fingerprint}', but got '#{peer_fingerprint}'"
79
-
80
- return false
82
+ "Expected fingerprint '#{fingerprint}', but got '#{peer_fingerprint}'"
81
83
  end
82
84
  end
83
85
 
@@ -85,11 +87,7 @@ module Blur
85
87
  ca_certificate = OpenSSL::X509::Certificate.new File.read ssl_cert_file
86
88
  valid_signature = peer_certificate.verify ca_certificate.public_key
87
89
 
88
- if not valid_signature
89
- raise SSLValidationError, "Certificate verify failed"
90
-
91
- return false
92
- end
90
+ raise SSLValidationError, 'Certificate verify failed' unless valid_signature
93
91
  end
94
92
 
95
93
  true
@@ -98,9 +96,7 @@ module Blur
98
96
  # Called once the connection is finally established.
99
97
  def connection_completed
100
98
  # We aren't completely connected yet if the connection is encrypted.
101
- unless @network.secure?
102
- connected!
103
- end
99
+ connected! unless @network.secure?
104
100
  end
105
101
 
106
102
  # Called just as the connection is being terminated, either by remote or
@@ -112,7 +108,8 @@ module Blur
112
108
  super
113
109
  end
114
110
 
115
- private
111
+ private
112
+
116
113
  # Called when connection has been established.
117
114
  def connected!
118
115
  @connected = true
@@ -122,12 +119,12 @@ module Blur
122
119
 
123
120
  # Returns true if we're expected to verify the certificate fingerprint.
124
121
  def fingerprint_verification?
125
- not @network.options[:ssl_fingerprint].nil?
122
+ !@network.options[:ssl_fingerprint].nil?
126
123
  end
127
124
 
128
125
  # Returns true if we should verify the peer certificate.
129
126
  def certificate_verification?
130
- not @network.options[:ssl_cert_file].nil?
127
+ !@network.options[:ssl_cert_file].nil?
131
128
  end
132
129
 
133
130
  # Get the hexadecimal representation of the certificates public key.
@@ -143,9 +140,8 @@ module Blur
143
140
  fingerprint = @network.options[:ssl_fingerprint]
144
141
 
145
142
  raise SSLValidationError,
146
- "Expected fingerprint '#{fingerprint}' but got '#{peer_fingerprint}'"
143
+ "Expected fingerprint '#{fingerprint}' but got '#{peer_fingerprint}'"
147
144
  end
148
-
149
145
  end
150
146
  end
151
147
  end
@@ -1,20 +1,22 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
2
4
 
3
5
  module Blur
4
6
  class Network
5
7
  # ISupport class that enables servers to announce what they support.
6
- #
8
+ #
7
9
  # @see https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03
8
10
  class ISupport < Hash
9
11
  # Return the network reference.
10
12
  attr_accessor :network
11
13
 
12
14
  # ISUPPORT parameters which should always be casted to numeric values.
13
- NumericParams = %w[CHANNELLEN MODES NICKLEN KICKLEN TOPICLEN AWAYLEN
14
- MAXCHANNELS MAXBANS MAXPARA MAXTARGETS].freeze
15
+ NUMERIC_PARAMS = %w[CHANNELLEN MODES NICKLEN KICKLEN TOPICLEN AWAYLEN
16
+ MAXCHANNELS MAXBANS MAXPARA MAXTARGETS].freeze
15
17
 
16
18
  # Our parsers for parameters that require special treatment.
17
- Parsers = {
19
+ PARSERS = {
18
20
  # CHANLIMIT=pfx:num[,pfx:num,...]
19
21
  #
20
22
  # This parameter specifies the maximum number of channels that a client
@@ -24,10 +26,10 @@ module Blur
24
26
  # the client may join in total. If there is no limit to the number of
25
27
  # certain channel type(s) a client may join, the limit should be
26
28
  # specified as the empty string, for example "#:".
27
- %w[CHANLIMIT] => -> (value) do
28
- Hash.new.tap do |result|
29
- params = value.split ?,
30
- mappings = params.map{|param| param.split ?: }
29
+ %w[CHANLIMIT] => lambda do |value|
30
+ {}.tap do |result|
31
+ params = value.split ','
32
+ mappings = params.map { |param| param.split ':' }
31
33
 
32
34
  mappings.each do |prefixes, limit|
33
35
  prefixes.each_char do |prefix|
@@ -48,10 +50,12 @@ module Blur
48
50
  #
49
51
  # The order of the modes is from that which gives most privileges on
50
52
  # the channel, to that which gives the least.
51
- %w[PREFIX] => -> (value) do
52
- Hash.new.tap do |result|
53
- if value =~ /^\((.+)\)(.*)/
54
- modes, prefix = $~[1..2]
53
+ %w[PREFIX] => lambda do |value|
54
+ {}.tap do |result|
55
+ match = value.match(/^\((.+)\)(.*)/)
56
+
57
+ if match
58
+ modes, prefix = match[1..2]
55
59
 
56
60
  modes.chars.each_with_index do |char, index|
57
61
  result[char] = prefix[index]
@@ -77,39 +81,41 @@ module Blur
77
81
  # mode is removed both in the client's and server's MODE command.
78
82
  # o Type D: Modes that change a setting on the channel. These modes
79
83
  # never take a parameter.
80
- %w[CHANMODES] => -> (value) do
81
- Hash.new.tap do |r|
82
- r["A"], r["B"], r["C"], r["D"] = value.split(?,).map &:chars
84
+ %w[CHANMODES] => lambda do |value|
85
+ {}.tap do |r|
86
+ r['A'], r['B'], r['C'], r['D'] = value.split(',').map &:chars
83
87
  end
84
88
  end,
85
89
 
86
90
  # Cast known params that are numeric, to a numeric value.
87
- NumericParams => -> (value) do
91
+ NUMERIC_PARAMS => lambda do |value|
88
92
  value.to_i
89
93
  end
90
- }
94
+ }.freeze
91
95
 
92
96
  # Initialize a new ISupport with a network reference.
93
- #
97
+ #
94
98
  # @param network [Network] The parent network.
95
99
  def initialize network
100
+ super
101
+
96
102
  @network = network
97
103
 
98
104
  # Set default ISUPPORT values.
99
105
  #
100
106
  # @see
101
107
  # https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#appendix-A
102
- self["MODES"] = 3
103
- self["PREFIX"] = { "o" => "@", "v" => "+" }
104
- self["KICKLEN"] = 200
105
- self["NICKLEN"] = 9
106
- self["MAXLIST"] = { "#" => Float::INFINITY, "&" => Float::INFINITY }
108
+ self['MODES'] = 3
109
+ self['PREFIX'] = { 'o' => '@', 'v' => '+' }
110
+ self['KICKLEN'] = 200
111
+ self['NICKLEN'] = 9
112
+ self['MAXLIST'] = { '#' => Float::INFINITY, '&' => Float::INFINITY }
107
113
  self['TOPICLEN'] = 200
108
- self["CHANMODES"] = {}
109
- self["CHANTYPES"] = %w{# &}
110
- self["CHANLIMIT"] = { "#" => Float::INFINITY, "&" => Float::INFINITY }
111
- self["CHANNELLEN"] = 200
112
- self["CASEMAPPING"] = "rfc1459"
114
+ self['CHANMODES'] = {}
115
+ self['CHANTYPES'] = %w[# &]
116
+ self['CHANLIMIT'] = { '#' => Float::INFINITY, '&' => Float::INFINITY }
117
+ self['CHANNELLEN'] = 200
118
+ self['CASEMAPPING'] = 'rfc1459'
113
119
  end
114
120
 
115
121
  # Parse a list of parameters to see what the server supports.
@@ -117,12 +123,12 @@ module Blur
117
123
  # @param parameters [Array] The list of parameters.
118
124
  def parse *params
119
125
  params.each do |parameter|
120
- name, value = parameter.split ?=
126
+ name, value = parameter.split '='
121
127
 
122
128
  if value
123
- _, parser = Parsers.find{|key, value| key.include? name }
129
+ _, parser = PARSERS.find { |key, _value| key.include? name }
124
130
 
125
- self[name] = parser.nil? ? value : parser.(value)
131
+ self[name] = parser.nil? ? value : parser.call(value)
126
132
  else
127
133
  self[name] = true
128
134
  end
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Blur
4
4
  # The +Network+ module is to be percieved as an IRC network.
@@ -6,44 +6,81 @@ module Blur
6
6
  # Although the connection is a part of the network module, it is mainly used
7
7
  # for network-related structures, such as {User}, {Channel} and {Command}.
8
8
  class Network
9
- include Logging
10
-
11
9
  # +ConnectionError+ should only be triggered from within {Connection}.
12
10
  class ConnectionError < StandardError; end
13
11
 
12
+ DEFAULT_PING_INTERVAL = 30
13
+ DEFAULT_RECONNECT = true
14
+
15
+ # Returns a unique identifier for this network.
16
+ #
17
+ # You can override the id in your network configuration by setting an 'id'
18
+ # key with the id you want.. If no id is specified, the the id will be
19
+ # constructed from the hostname and port number
20
+ # in the format "<host>:<port>"
21
+ #
22
+ # @return [String] the unique identifier for this network.
23
+ attr_reader :id
24
+ # @return [String] the current nickname.
25
+ attr_accessor :nickname
14
26
  # @return [Hash] the network options.
15
27
  attr_accessor :options
16
- # @return [Array] the list of channels the client is in.
28
+ # @return [Hash] the map of users that is known.
29
+ attr_accessor :users
30
+ # @return [Hash] the map of channels the client is in.
17
31
  attr_accessor :channels
18
- # @return [Array] the list of private messages the client remembers.
19
- attr_accessor :dialogues
20
- # @return [Client] the client delegate.
21
- attr_accessor :delegate
32
+ # @return [Client] the client reference.
33
+ attr_accessor :client
22
34
  # @return [Network::Connection] the connection instance.
23
35
  attr_accessor :connection
24
36
  # @return [Network::ISupport] the network isupport specs.
25
37
  attr_accessor :isupport
38
+ # @return [Array<String>] list of capabilities supported by the network.
39
+ attr_accessor :capabilities
40
+ # @return [Boolean] true if we're waiting for a capability negotiation.
41
+ attr_reader :waiting_for_cap
42
+ # @return [Time] the last time a pong was sent or received.
43
+ attr_accessor :last_pong_time
44
+ # The max PING interval for the server. This is used to determine when the
45
+ # client will attempt to send its own PING command.
46
+ #
47
+ # @note the actual time until a client PING is sent can vary by an
48
+ # additional 0-30 seconds.
49
+ # @return [Number] the max interval between pings from a server.
50
+ attr_accessor :server_ping_interval_max
26
51
 
27
52
  # Check whether or not connection is established.
28
- def connected?; @connection and @connection.established? end
53
+ def connected?
54
+ @connection&.established?
55
+ end
29
56
 
30
57
  # Get the remote hostname.
31
58
  #
32
59
  # @return [String] the remote hostname.
33
- def host; @options[:hostname] end
60
+ def host
61
+ @options['hostname']
62
+ end
34
63
 
35
64
  # Get the remote port.
36
65
  # If no port is specified, it returns 6697 if using a secure connection,
37
66
  # returns 6667 otherwise.
38
67
  #
39
68
  # @return [Fixnum] the remote port
40
- def port; @options[:port] ||= secure? ? 6697 : 6667 end
41
-
69
+ def port
70
+ @options['port'] ||= secure? ? 6697 : 6667
71
+ end
72
+
42
73
  # Check to see if it's a secure connection.
43
- def secure?; @options[:secure] == true end
74
+ def secure?
75
+ @options['secure'] == true
76
+ end
44
77
 
45
- # Check to see if FiSH encryption is enabled.
46
- def fish?; not @options[:fish].nil? end
78
+ # @return [Boolean] whether we want to authenticate with SASL.
79
+ def sasl?
80
+ @options['sasl'] &&
81
+ @options['sasl']['username'] &&
82
+ @options['sasl']['password']
83
+ end
47
84
 
48
85
  # Instantiates the network.
49
86
  #
@@ -54,51 +91,64 @@ module Blur
54
91
  # @option options [optional, String] :username (Copies :nickname)
55
92
  # The username to use. This is also known as the ident.
56
93
  # @option options [optional, String] :realname (Copies :username)
57
- # The real name that we want to use. This is usually what shows up
94
+ # The "real name" that we want to use. This is usually what shows up
58
95
  # as "Name" when you whois a user.
59
96
  # @option options [optional, String] :password The password for the network.
60
97
  # This is sometimes needed for private networks.
61
- # @option options [optional, Fixnum] :port (6697 if ssl, otherwise 6667)
98
+ # @option options [optional, Fixnum] :port (6697 if ssl, otherwise 6667)
62
99
  # The remote port we want to connect to.
63
100
  # @option options [optional, Boolean] :secure Set whether this is a secure
64
101
  # (SSL-encrypted) connection.
65
102
  # @option options [optional, String] :ssl_cert_file Local path of a
66
103
  # readable file that contains a X509 CA certificate to validate against.
67
- # @option options [optional, String] :ssl_fingerprint Validate that the
104
+ # @option options [optional, String] :ssl_fingerprint Validate that the
68
105
  # remote certificate matches the specified fingerprint.
69
106
  # @option options [optional, Boolean] :ssl_no_verify Disable verification
70
107
  # alltogether.
71
- def initialize options
72
- @options = options
73
- @channels = []
108
+ def initialize options, client = nil
109
+ @client = client
110
+ @options = options
111
+ # @log = ::Logging.logger[self]
112
+ @users = {}
113
+ @channels = {}
74
114
  @isupport = ISupport.new self
75
-
76
- unless options[:nickname]
77
- raise ArgumentError, "nickname is missing from the networks option block"
115
+ @capabilities = []
116
+ @reconnect_interval = 3
117
+ @server_ping_interval_max = @options.fetch('server_ping_interval',
118
+ 150).to_i
119
+
120
+ unless options['nickname']
121
+ raise ArgumentError, 'Network configuration for ' \
122
+ "`#{id}' is missing a nickname"
78
123
  end
79
-
80
- @options[:username] ||= @options[:nickname]
81
- @options[:realname] ||= @options[:username]
82
- @options[:channels] ||= []
124
+
125
+ @nickname = options['nickname']
126
+ @options['username'] ||= @options['nickname']
127
+ @options['realname'] ||= @options['username']
128
+ @options['channels'] ||= []
129
+ @id = options.fetch 'id', "#{host}:#{port}"
83
130
  end
84
-
131
+
85
132
  # Send a message to a recipient.
86
133
  #
87
134
  # @param [String, #to_s] recipient the recipient.
88
135
  # @param [String] message the message.
89
136
  def say recipient, message
90
- if recipient.is_a? Channel and recipient.encrypted?
91
- message = "+OK #{recipient.encryption.encrypt message}"
92
- end
93
-
94
137
  transmit :PRIVMSG, recipient.to_s, message
95
138
  end
96
-
139
+
140
+ # Forwards the received message to the client instance.
141
+ #
97
142
  # Called when the network connection has enough data to form a command.
98
- def got_command command
99
- @delegate.got_command self, command
143
+ def got_message message
144
+ @client.got_message self, message
145
+ rescue StandardError => e
146
+ puts "#{e.class}: #{e.message}"
147
+ puts
148
+ puts '---'
149
+ puts e.backtrace
100
150
  end
101
-
151
+
102
152
  # Find a channel by its name.
103
153
  #
104
154
  # @param [String] name the channel name.
@@ -106,7 +156,7 @@ module Blur
106
156
  def channel_by_name name
107
157
  @channels.find { |channel| channel.name == name }
108
158
  end
109
-
159
+
110
160
  # Find all instances of channels in which there is a user with the nick
111
161
  # +nick+.
112
162
  #
@@ -120,65 +170,153 @@ module Blur
120
170
  #
121
171
  # @return [Array<String>] a list of user prefixes.
122
172
  def user_prefixes
123
- isupport["PREFIX"].values
173
+ isupport['PREFIX'].values
124
174
  end
125
175
 
126
176
  # Returns a list of user modes that also gives a users nick a prefix.
127
177
  #
128
178
  # @return [Array<String>] a list of user modes.
129
179
  def user_prefix_modes
130
- isupport["PREFIX"].keys
180
+ isupport['PREFIX'].keys
131
181
  end
132
182
 
133
183
  # Returns a list of channel flags (channel mode D).
134
184
  #
135
185
  # @return [Array<String>] a list of channel flags.
136
186
  def channel_flags
137
- isupport["CHANMODES"]["D"]
187
+ isupport['CHANMODES']['D']
138
188
  end
139
189
 
140
190
  # Attempt to establish a connection and send initial data.
141
191
  #
142
192
  # @see Connection
143
193
  def connect
144
- @connection = EventMachine.connect host, port, Connection, self
194
+ # @log.info "Connecting to #{self}"
195
+
196
+ begin
197
+ @connection = EventMachine.connect host, port, Connection, self
198
+ rescue EventMachine::ConnectionError => e
199
+ warn "Establishing connection to #{self} failed!"
200
+ warn e.message
201
+
202
+ schedule_reconnect
203
+ return
204
+ end
205
+
206
+ @ping_timer = EventMachine.add_periodic_timer DEFAULT_PING_INTERVAL do
207
+ periodic_ping_check
208
+ end
209
+ end
210
+
211
+ # Schedules a reconnect after a user-specified number of seconds.
212
+ def schedule_reconnect
213
+ # @log.info "Reconnecting to #{self} in #{@reconnect_interval} seconds"
214
+
215
+ EventMachine.add_timer @reconnect_interval do
216
+ connect
217
+ end
218
+ end
219
+
220
+ def server_connection_timeout
221
+ @connection.close_connection
222
+
223
+ warn "Connection to #{self} timed out"
224
+ end
225
+
226
+ def periodic_ping_check
227
+ now = Time.now
228
+ seconds_since_pong = now - @last_pong_time
229
+
230
+ return unless seconds_since_pong >= @server_ping_interval_max
231
+
232
+ # @log.info "No PING request from the server in #{seconds_since_pong}s!"
233
+
234
+ transmit 'PING', now.to_s
235
+
236
+ # Wait 15 seconds and declare a timeout if we didn't get a PONG.
237
+ previous_pong_time = @last_pong_time.dup
238
+
239
+ EventMachine.add_timer 15 do
240
+ server_connection_timeout if @last_pong_time == previous_pong_time
241
+ end
145
242
  end
146
243
 
147
244
  # Called when the connection was successfully established.
148
245
  def connected!
149
- transmit :PASS, @options[:password] if @options[:password]
150
- transmit :NICK, @options[:nickname]
151
- transmit :USER, @options[:username], :void, :void, @options[:realname]
246
+ @waiting_for_cap = true
247
+ @capabilities.clear
248
+
249
+ transmit :CAP, 'LS'
250
+ transmit :PASS, @options['password'] if @options['password']
251
+ transmit :NICK, @options['nickname']
252
+ transmit :USER, @options['username'], 'void', 'void', @options['realname']
253
+
254
+ @last_pong_time = Time.now
255
+ end
256
+
257
+ # Called when the server doesn't support capability negotiation.
258
+ def abort_cap_neg
259
+ @waiting_for_cap = false
260
+
261
+ puts 'Server does not support capability negotiation'
262
+ end
263
+
264
+ # Called when we're done with capability negotiation.
265
+ def cap_end
266
+ @waiting_for_cap = false
267
+
268
+ transmit :CAP, 'END'
152
269
  end
153
270
 
154
271
  # Called when the connection was closed.
155
272
  def disconnected!
156
- @channels.each { |channel| channel.users.clear }
273
+ @channels.each { |_, channel| channel.users.clear }
157
274
  @channels.clear
275
+ @users.clear
276
+ @ping_timer.cancel
277
+
278
+ # @log.debug "Connection to #{self} lost!"
279
+ @client.network_connection_closed self
158
280
 
159
- @delegate.network_connection_closed self
281
+ return unless @options.fetch('reconnect', DEFAULT_RECONNECT)
282
+
283
+ schedule_reconnect
160
284
  end
161
-
285
+
162
286
  # Terminate the connection and clear all channels and users.
163
287
  def disconnect
164
288
  @connection.close_connection_after_writing
165
289
  end
166
-
290
+
167
291
  # Transmit a command to the server.
168
292
  #
169
293
  # @param [Symbol, String] name the command name.
170
294
  # @param [...] arguments all the prepended parameters.
171
295
  def transmit name, *arguments
172
- command = Command.new name, arguments
173
- log "#{'→' ^ :red} #{command.name.to_s.ljust(8, ' ') ^ :light_gray} #{command.params.map(&:inspect).join ' '}"
174
-
175
- @connection.send_data "#{command}\r\n"
296
+ message = IRCParser::Message.new command: name.to_s, parameters: arguments
297
+
298
+ if @client.verbose
299
+ formatted_command = message.command.to_s.ljust 8, ' '
300
+ formatted_params = message.parameters.map(&:inspect).join ' '
301
+ puts "→ #{formatted_command} #{formatted_params}"
302
+ end
303
+
304
+ @connection.send_data "#{message}\r\n"
305
+ end
306
+
307
+ # Send a private message.
308
+ def send_privmsg recipient, message
309
+ transmit :PRIVMSG, recipient, message
310
+ end
311
+
312
+ # Join a channel.
313
+ def join channel
314
+ transmit :JOIN, channel
176
315
  end
177
316
 
178
-
179
317
  # Convert it to a debug-friendly format.
180
318
  def to_s
181
- %{#<#{self.class.name} "#{host}":#{port}>}
319
+ %(#<#{self.class.name} "#{host}":#{port}>)
182
320
  end
183
321
  end
184
322
  end