crussh 0.1.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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +371 -0
  5. data/ext/poly1305/Cargo.toml +13 -0
  6. data/ext/poly1305/extconf.rb +6 -0
  7. data/ext/poly1305/src/lib.rs +75 -0
  8. data/lib/crussh/auth.rb +46 -0
  9. data/lib/crussh/channel/key_parser.rb +125 -0
  10. data/lib/crussh/channel.rb +381 -0
  11. data/lib/crussh/cipher/algorithm.rb +31 -0
  12. data/lib/crussh/cipher/chacha20poly1305.rb +98 -0
  13. data/lib/crussh/cipher.rb +25 -0
  14. data/lib/crussh/compression.rb +42 -0
  15. data/lib/crussh/gatekeeper.rb +50 -0
  16. data/lib/crussh/handler/line_buffer.rb +131 -0
  17. data/lib/crussh/handler.rb +128 -0
  18. data/lib/crussh/heartbeat.rb +68 -0
  19. data/lib/crussh/kex/algorithm.rb +86 -0
  20. data/lib/crussh/kex/curve25519.rb +30 -0
  21. data/lib/crussh/kex/exchange.rb +234 -0
  22. data/lib/crussh/kex.rb +42 -0
  23. data/lib/crussh/keys/key_pair.rb +61 -0
  24. data/lib/crussh/keys/public_key.rb +35 -0
  25. data/lib/crussh/keys.rb +70 -0
  26. data/lib/crussh/limits.rb +45 -0
  27. data/lib/crussh/logger.rb +95 -0
  28. data/lib/crussh/mac/algorithm.rb +23 -0
  29. data/lib/crussh/mac/crypto.rb +60 -0
  30. data/lib/crussh/mac/none.rb +9 -0
  31. data/lib/crussh/mac.rb +28 -0
  32. data/lib/crussh/negotiator.rb +41 -0
  33. data/lib/crussh/preferred.rb +16 -0
  34. data/lib/crussh/protocol/channel_close.rb +11 -0
  35. data/lib/crussh/protocol/channel_data.rb +12 -0
  36. data/lib/crussh/protocol/channel_eof.rb +11 -0
  37. data/lib/crussh/protocol/channel_extended_data.rb +13 -0
  38. data/lib/crussh/protocol/channel_failure.rb +11 -0
  39. data/lib/crussh/protocol/channel_open.rb +69 -0
  40. data/lib/crussh/protocol/channel_open_confirmation.rb +15 -0
  41. data/lib/crussh/protocol/channel_open_failure.rb +14 -0
  42. data/lib/crussh/protocol/channel_request.rb +146 -0
  43. data/lib/crussh/protocol/channel_success.rb +11 -0
  44. data/lib/crussh/protocol/channel_window_adjust.rb +12 -0
  45. data/lib/crussh/protocol/debug.rb +15 -0
  46. data/lib/crussh/protocol/disconnect.rb +39 -0
  47. data/lib/crussh/protocol/ext_info.rb +48 -0
  48. data/lib/crussh/protocol/global_request.rb +46 -0
  49. data/lib/crussh/protocol/ignore.rb +11 -0
  50. data/lib/crussh/protocol/kex_ecdh_init.rb +11 -0
  51. data/lib/crussh/protocol/kex_ecdh_reply.rb +13 -0
  52. data/lib/crussh/protocol/kex_init.rb +38 -0
  53. data/lib/crussh/protocol/new_keys.rb +9 -0
  54. data/lib/crussh/protocol/ping.rb +11 -0
  55. data/lib/crussh/protocol/pong.rb +11 -0
  56. data/lib/crussh/protocol/request_failure.rb +9 -0
  57. data/lib/crussh/protocol/request_success.rb +11 -0
  58. data/lib/crussh/protocol/service_accept.rb +11 -0
  59. data/lib/crussh/protocol/service_request.rb +11 -0
  60. data/lib/crussh/protocol/unimplemented.rb +11 -0
  61. data/lib/crussh/protocol/userauth_banner.rb +12 -0
  62. data/lib/crussh/protocol/userauth_failure.rb +12 -0
  63. data/lib/crussh/protocol/userauth_pk_ok.rb +12 -0
  64. data/lib/crussh/protocol/userauth_request.rb +52 -0
  65. data/lib/crussh/protocol/userauth_success.rb +9 -0
  66. data/lib/crussh/protocol.rb +135 -0
  67. data/lib/crussh/server/auth_handler.rb +18 -0
  68. data/lib/crussh/server/config.rb +157 -0
  69. data/lib/crussh/server/layers/connection.rb +363 -0
  70. data/lib/crussh/server/layers/transport.rb +49 -0
  71. data/lib/crussh/server/layers/userauth.rb +232 -0
  72. data/lib/crussh/server/request_rule.rb +76 -0
  73. data/lib/crussh/server/session.rb +192 -0
  74. data/lib/crussh/server.rb +214 -0
  75. data/lib/crussh/ssh_id.rb +44 -0
  76. data/lib/crussh/transport/packet_stream.rb +245 -0
  77. data/lib/crussh/transport/reader.rb +98 -0
  78. data/lib/crussh/transport/version_exchange.rb +26 -0
  79. data/lib/crussh/transport/writer.rb +72 -0
  80. data/lib/crussh/version.rb +5 -0
  81. data/lib/crussh.rb +61 -0
  82. data/sig/crussh.rbs +4 -0
  83. metadata +249 -0
@@ -0,0 +1,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "io/stream"
5
+ require "async/semaphore"
6
+
7
+ module Crussh
8
+ class Channel
9
+ DEFAULT_WINDOW_SIZE = 2 * 1024 * 1024
10
+ DEFAULT_MAX_PACKET_SIZE = 32_768
11
+
12
+ Data = ::Data.define(:data) do
13
+ def each_key(parser: KeyParser.new, &block)
14
+ return enum_for(:each_key, parser: parser) unless block_given?
15
+
16
+ parser.parse(data).each(&block)
17
+ end
18
+ end
19
+ ExtendedData = ::Data.define(:data, :type)
20
+ WindowChange = ::Data.define(:width, :height, :pixel_width, :pixel_height)
21
+ Signal = ::Data.define(:name)
22
+ EOF = ::Data.define
23
+ Closed = ::Data.define
24
+
25
+ Pty = ::Data.define(:term, :width, :height, :pixel_width, :pixel_height, :modes)
26
+
27
+ def initialize(session:, id:, remote_id:, remote_window_size:, local_window_size:, max_packet_size:, buffer_size:)
28
+ @session = session
29
+ @id = id
30
+ @remote_id = remote_id
31
+
32
+ @reader = Reader.new(
33
+ session:,
34
+ remote_id:,
35
+ window_size: local_window_size,
36
+ buffer_size:,
37
+ )
38
+
39
+ @writer = Writer.new(
40
+ session:,
41
+ remote_id:,
42
+ max_packet_size:,
43
+ window_size: remote_window_size,
44
+ channel: self,
45
+ )
46
+
47
+ @pty = nil
48
+ @env = {}
49
+ @events = Async::Queue.new
50
+ @read_buffer = "".b
51
+ end
52
+
53
+ attr_reader :session, :id, :remote_id, :pty, :env
54
+ attr_writer :pty
55
+
56
+ def pty? = !@pty.nil?
57
+ def eof? = @reader.eof?
58
+ def closed? = @writer.closed?
59
+
60
+ def read(...) = @reader.read(...)
61
+ def readpartial(...) = @reader.readpartial(...)
62
+ def gets(...) = @reader.gets(...)
63
+ def each(&) = @reader.each(&)
64
+
65
+ def write(...) = @writer.write(...)
66
+ def puts(...) = @writer.puts(...)
67
+ def print(...) = @writer.print(...)
68
+ def flush = @writer.flush
69
+
70
+ def <<(data)
71
+ write(data)
72
+ self
73
+ end
74
+
75
+ def send_eof = @writer.send_eof
76
+ def close = @writer.close
77
+
78
+ def stderr
79
+ @stderr ||= Stderr.new(session:, remote_id:)
80
+ end
81
+
82
+ def exit_status(code)
83
+ message = Protocol::ChannelRequest.new(recipient_channel: @remote_id, request_type: "exit-status", want_reply: false, request_data: [code].pack("N"))
84
+ @session.write_packet(message)
85
+ end
86
+
87
+ def exit_signal(signal_name, core_dumped: false, error_message: "", language: "")
88
+ writer = Transport::Writer.new
89
+ writer.string(signal_name)
90
+ writer.boolean(core_dumped)
91
+ writer.string(error_message)
92
+ writer.string(language)
93
+
94
+ message = Protocol::ChannelRequest.new(recipient_channel: @remote_id, request_type: "exit-signal", want_reply: false, request_data: writer.to_s)
95
+ @session.write_packet(message)
96
+ end
97
+
98
+ def push_event(event) = @reader.push_event(event)
99
+ def adjust_remote_window(bytes) = @writer.adjust_window(bytes)
100
+
101
+ def set_env(name, value)
102
+ @env[name] = value
103
+ end
104
+
105
+ def update_window(window_change)
106
+ @pty = @pty.with(width: window_change.width, height: window_change.height, pixel_width: window_change.pixel_width, pixel_height: window_change.pixel_height) if @pty
107
+ push_event(window_change)
108
+ end
109
+
110
+ class Reader
111
+ def initialize(session:, remote_id:, window_size:, buffer_size:)
112
+ @session = session
113
+ @remote_id = remote_id
114
+ @window_size = window_size
115
+ @window_threshold = window_size / 2
116
+ @bytes_consumed = 0
117
+
118
+ @events = Async::LimitedQueue.new(buffer_size)
119
+ @buffer = "".b
120
+
121
+ @eof = false
122
+ @closed = false
123
+ end
124
+
125
+ def eof? = @eof
126
+ def closed? = @closed
127
+
128
+ def push_event(event)
129
+ @events.enqueue(event)
130
+ end
131
+
132
+ def each
133
+ return enum_for(:each) unless block_given?
134
+
135
+ loop do
136
+ event = @events.dequeue
137
+ yield event
138
+ break if event.is_a?(Closed)
139
+ end
140
+ end
141
+
142
+ def read(length = nil)
143
+ return drain_buffer(length) if length && @buffer.bytesize >= length
144
+
145
+ loop do
146
+ event = @events.dequeue
147
+
148
+ case event
149
+ when Data(data:)
150
+ consume_window(data.bytesize)
151
+ @buffer << data
152
+ return drain_buffer(length) if length && @buffer.bytesize >= length
153
+ when EOF
154
+ @eof = true
155
+
156
+ return @buffer.empty? ? nil : drain_buffer(length)
157
+ when Closed
158
+ @closed = true
159
+
160
+ return @buffer.empty? ? nil : drain_buffer(length)
161
+ end
162
+ end
163
+ end
164
+
165
+ def readpartial(maxlen, outbuf = nil)
166
+ outbuf ||= "".b
167
+ outbuf.clear
168
+
169
+ if @buffer.bytesize > 0
170
+ outbuf << drain_buffer(maxlen)
171
+ return outbuf
172
+ end
173
+
174
+ loop do
175
+ event = @events.dequeue
176
+
177
+ case event
178
+ when Data(data:)
179
+ consume_window(data.bytesize)
180
+ @buffer << data
181
+ outbuf << drain_buffer(maxlen)
182
+ return outbuf
183
+ when EOF
184
+ @eof = true
185
+ raise ::EOFError, "end of file reached"
186
+ when Closed
187
+ @closed = true
188
+ raise ::IOError, "channel closed"
189
+ end
190
+ end
191
+ end
192
+
193
+ def gets(sep = $INPUT_RECORD_SEPARATOR, limit = nil)
194
+ loop do
195
+ if (index = @buffer.index(sep))
196
+ return @buffer.slice!(0, index + sep.bytesize)
197
+ end
198
+
199
+ return drain_buffer if limit && @buffer.bytesize >= limit
200
+
201
+ event = @events.dequeue
202
+
203
+ case event
204
+ when Data
205
+ consume_window(event.data.bytesize)
206
+ @buffer << event.data
207
+ when EOF
208
+ @eof = true
209
+ return @buffer.empty? ? nil : @buffer.slice!(0..-1)
210
+ when Closed
211
+ @closed = true
212
+ return @buffer.empty? ? nil : @buffer.slice!(0..-1)
213
+ end
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ def drain_buffer(length = nil)
220
+ @buffer.slice!(0, length || @buffer.bytesize)
221
+ end
222
+
223
+ def consume_window(bytes)
224
+ @bytes_consumed += bytes
225
+
226
+ return if @bytes_consumed < @window_threshold
227
+
228
+ message = Protocol::ChannelWindowAdjust.new(
229
+ recipient_channel: @remote_id,
230
+ bytes_to_add: @bytes_consumed,
231
+ )
232
+
233
+ @session.write_packet(message)
234
+ @bytes_consumed = 0
235
+ end
236
+ end
237
+
238
+ module WriteMethods
239
+ def line_ending
240
+ "\n"
241
+ end
242
+
243
+ def puts(*args)
244
+ if args.empty?
245
+ write(line_ending)
246
+ return
247
+ end
248
+
249
+ buffer = "".b
250
+ args.each do |arg|
251
+ line = arg.to_s
252
+ buffer << line
253
+ buffer << line_ending unless line.end_with?("\n")
254
+ end
255
+ write(buffer)
256
+ nil
257
+ end
258
+
259
+ def print(*args)
260
+ args.each { |arg| write(arg.to_s) }
261
+ nil
262
+ end
263
+
264
+ def <<(data)
265
+ write(data)
266
+ self
267
+ end
268
+
269
+ def flush = self
270
+ end
271
+
272
+ class Writer
273
+ include WriteMethods
274
+
275
+ def initialize(session:, remote_id:, max_packet_size:, window_size:, channel:)
276
+ @session = session
277
+ @remote_id = remote_id
278
+ @max_packet_size = max_packet_size
279
+ @window_size = window_size
280
+ @channel = channel
281
+
282
+ @window_condition = Async::Condition.new
283
+ @semaphore = Async::Semaphore.new(1)
284
+
285
+ @eof_sent = false
286
+ @closed = false
287
+ end
288
+
289
+ def closed? = @closed
290
+
291
+ def line_ending
292
+ @channel.pty? ? "\r\n" : "\n"
293
+ end
294
+
295
+ def adjust_window(...) = @semaphore.acquire { adjust_window_inner(...) }
296
+ def write(...) = @semaphore.acquire { write_inner(...) }
297
+ def send_eof = @semaphore.acquire { send_eof_inner }
298
+ def close = @semaphore.acquire { close_inner }
299
+
300
+ private
301
+
302
+ def adjust_window_inner(bytes)
303
+ @window_size += bytes
304
+ @window_condition.signal
305
+ end
306
+
307
+ def close_inner
308
+ return if @closed
309
+
310
+ send_eof_inner unless @eof_sent
311
+ @closed = true
312
+
313
+ message = Protocol::ChannelClose.new(recipient_channel: @remote_id)
314
+ @session.write_packet(message)
315
+ end
316
+
317
+ def send_eof_inner
318
+ return if @eof_sent
319
+
320
+ @eof_sent = true
321
+
322
+ message = Protocol::ChannelEof.new(recipient_channel: @remote_id)
323
+ @session.write_packet(message)
324
+ end
325
+
326
+ def write_inner(data)
327
+ raise ::IOError, "channel closed" if @closed
328
+ raise ::IOError, "EOF already sent" if @eof_sent
329
+
330
+ data = data.to_s.b
331
+ bytes_written = 0
332
+
333
+ while bytes_written < data.bytesize
334
+ @window_condition.wait if @window_size <= 0
335
+
336
+ chunk_size = [
337
+ @max_packet_size,
338
+ @window_size,
339
+ data.bytesize - bytes_written,
340
+ ].min
341
+ chunk = data.byteslice(bytes_written, chunk_size)
342
+
343
+ message = Protocol::ChannelData.new(recipient_channel: @remote_id, data: chunk)
344
+ @session.write_packet(message)
345
+
346
+ @window_size -= chunk_size
347
+ bytes_written += chunk_size
348
+ end
349
+
350
+ bytes_written
351
+ end
352
+ end
353
+
354
+ class Stderr
355
+ include WriteMethods
356
+
357
+ def initialize(session:, remote_id:)
358
+ @session = session
359
+ @remote_id = remote_id
360
+ @semaphore = Async::Semaphore.new(1)
361
+ end
362
+
363
+ def write(...) = @semaphore.acquire { write_inner(...) }
364
+
365
+ private
366
+
367
+ def write_inner(data)
368
+ data = data.to_s.b
369
+
370
+ message = Protocol::ChannelExtendedData.new(
371
+ recipient_channel: @remote_id,
372
+ data_type_code: 1,
373
+ data: data,
374
+ )
375
+ @session.write_packet(message)
376
+
377
+ data.bytesize
378
+ end
379
+ end
380
+ end
381
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Cipher
5
+ class Algorithm
6
+ def key_length
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def nonce_length
11
+ 0
12
+ end
13
+
14
+ def block_size
15
+ 8
16
+ end
17
+
18
+ def needs_mac?
19
+ true
20
+ end
21
+
22
+ def make_opening_key(key:, nonce:, mac_key:, mac:)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def make_sealing_key(key:, nonce:, mac_key:, mac:)
27
+ raise NotImplementedError
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Cipher
5
+ class ChaCha20Poly1305 < Algorithm
6
+ KEY_LENGTH = 64
7
+
8
+ def key_length = KEY_LENGTH
9
+ def nonce_length = 0
10
+ def block_size = 8
11
+ def tag_length = 16
12
+
13
+ def needs_mac? = false
14
+
15
+ def make_opening_key(key:, nonce: nil, mac_key: nil, mac: nil)
16
+ OpeningKey.new(key)
17
+ end
18
+
19
+ def make_sealing_key(key:, nonce: nil, mac_key: nil, mac: nil)
20
+ SealingKey.new(key)
21
+ end
22
+
23
+ class Key
24
+ def initialize(key)
25
+ raise ArgumentError, "Key must be exactly #{KEY_LENGTH} bytes" unless key.bytesize == KEY_LENGTH
26
+
27
+ @main_key = key[0, 32]
28
+ @header_key = key[32, 32]
29
+ end
30
+
31
+ private
32
+
33
+ def build_nonce(sequence)
34
+ "\x00\x00\x00\x00" + [sequence].pack("Q>")
35
+ end
36
+
37
+ def chacha20_cipher
38
+ cipher = OpenSSL::Cipher.new("chacha20")
39
+ cipher.encrypt
40
+ cipher
41
+ end
42
+
43
+ def chacha20_block(key, nonce, counter)
44
+ cipher = chacha20_cipher
45
+ cipher.key = key
46
+ cipher.iv = [counter].pack("V") + nonce
47
+ cipher
48
+ end
49
+
50
+ def generate_poly_key(sequence)
51
+ nonce = build_nonce(sequence)
52
+ cipher = chacha20_block(@main_key, nonce, 0)
53
+ cipher.update("\x00" * 64)[0, 32]
54
+ end
55
+ end
56
+
57
+ class OpeningKey < Key
58
+ def decrypt_length(sequence, encrypted_length)
59
+ nonce = build_nonce(sequence)
60
+ cipher = chacha20_block(@header_key, nonce, 0)
61
+ cipher.update(encrypted_length)
62
+ end
63
+
64
+ def open(sequence, encrypted_length, ciphertext, tag)
65
+ poly_key = generate_poly_key(sequence)
66
+ data = encrypted_length + ciphertext
67
+
68
+ unless Crypto::Poly1305.verify(poly_key, tag, data)
69
+ raise DecryptionError, "Poly1305 authentication failed"
70
+ end
71
+
72
+ nonce = build_nonce(sequence)
73
+ cipher = chacha20_block(@main_key, nonce, 1)
74
+ cipher.update(ciphertext)
75
+ end
76
+ end
77
+
78
+ class SealingKey < Key
79
+ def encrypt_length(sequence, length_bytes)
80
+ nonce = build_nonce(sequence)
81
+ cipher = chacha20_block(@header_key, nonce, 0)
82
+ cipher.update(length_bytes)
83
+ end
84
+
85
+ def seal(sequence, encrypted_length, plaintext)
86
+ nonce = build_nonce(sequence)
87
+ cipher = chacha20_block(@main_key, nonce, 1)
88
+ ciphertext = cipher.update(plaintext)
89
+
90
+ poly_key = generate_poly_key(sequence)
91
+ tag = Crypto::Poly1305.auth(poly_key, encrypted_length + ciphertext)
92
+
93
+ [ciphertext, tag]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Cipher
5
+ CHACHA20_POLY1305 = "chacha20-poly1305@openssh.com"
6
+
7
+ DEFAULT = [CHACHA20_POLY1305]
8
+
9
+ ALL = [CHACHA20_POLY1305]
10
+
11
+ REGISTRY = {
12
+ CHACHA20_POLY1305 => ChaCha20Poly1305,
13
+ }
14
+
15
+ class << self
16
+ def from_name(name)
17
+ algorithm_class = REGISTRY[name]
18
+
19
+ raise UnknownAlgorithm, "Unknown cipher algorithm: #{name}" if algorithm_class.nil?
20
+
21
+ algorithm_class.new
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Crussh
6
+ module Compression
7
+ NONE = "none"
8
+ ZLIB = "zlib@openssh.com"
9
+
10
+ DEFAULT = [ZLIB, NONE].freeze
11
+
12
+ class << self
13
+ def from_name(name)
14
+ case name
15
+ when NONE
16
+ None.new
17
+ when ZLIB
18
+ Zlib.new
19
+ else
20
+ raise UnknownAlgorithm, "Unknown compression: #{name}"
21
+ end
22
+ end
23
+ end
24
+
25
+ class Compressor
26
+ def deflate(data) = data
27
+ def inflate(data) = data
28
+ end
29
+
30
+ class None < Compressor; end
31
+
32
+ class Zlib
33
+ def initialize
34
+ @deflator = ::Zlib::Deflate.new
35
+ @inflator = ::Zlib::Inflate.new
36
+ end
37
+
38
+ def deflate(data) = @deflator.deflate(data, ::Zlib::SYNC_FLUSH)
39
+ def inflate(data) = @inflator.inflate(data)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Gatekeeper
5
+ def initialize(max_connections:, max_unauthenticated:)
6
+ @max_connections = max_connections
7
+ @max_unauthenticated = max_unauthenticated
8
+
9
+ @mutex = Mutex.new
10
+ @total = 0
11
+ @unauthenticated = 0
12
+ end
13
+
14
+ attr_reader :total, :unauthenticated
15
+
16
+ def block? = !allowed?
17
+
18
+ def authenticate!
19
+ @mutex.synchronize do
20
+ @unauthenticated -= 1 if @unauthenticated > 0
21
+ end
22
+ end
23
+
24
+ def disconnect!(was_authenticated:)
25
+ @mutex.synchronize do
26
+ @total -= 1 if @total > 0
27
+ @unauthenticated -= 1 if !was_authenticated && @unauthenticated > 0
28
+ end
29
+ end
30
+
31
+ def stats
32
+ @mutex.synchronize do
33
+ { total: @total, unauthenticated: @unauthenticated }
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def allowed?
40
+ @mutex.synchronize do
41
+ return false if @max_connections && @total >= @max_connections
42
+ return false if @max_unauthenticated && @unauthenticated >= @max_unauthenticated
43
+
44
+ @total += 1
45
+ @unauthenticated += 1
46
+ true
47
+ end
48
+ end
49
+ end
50
+ end