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,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Protocol
5
+ ALGORITHM_CATEGORIES = [
6
+ :kex_algorithms,
7
+ :server_host_key_algorithms,
8
+ :cipher_client_to_server,
9
+ :cipher_server_to_client,
10
+ :mac_client_to_server,
11
+ :mac_server_to_client,
12
+ :compression_client_to_server,
13
+ :compression_server_to_client,
14
+ :languages_client_to_server,
15
+ :languages_server_to_client,
16
+ ].freeze
17
+
18
+ # Messages
19
+
20
+ DISCONNECT = 1
21
+ IGNORE = 2
22
+ UNIMPLEMENTED = 3
23
+ DEBUG = 4
24
+ SERVICE_REQUEST = 5
25
+ SERVICE_ACCEPT = 6
26
+ EXT_INFO = 7
27
+
28
+ KEXINIT = 20
29
+ NEWKEYS = 21
30
+
31
+ KEX_ECDH_INIT = 30
32
+ KEX_ECDH_REPLY = 31
33
+
34
+ USERAUTH_REQUEST = 50
35
+ USERAUTH_FAILURE = 51
36
+ USERAUTH_SUCCESS = 52
37
+ USERAUTH_BANNER = 53
38
+ USERAUTH_PK_OK = 60
39
+
40
+ GLOBAL_REQUEST = 80
41
+ REQUEST_SUCCESS = 81
42
+ REQUEST_FAILURE = 82
43
+
44
+ CHANNEL_OPEN = 90
45
+ CHANNEL_OPEN_CONFIRMATION = 91
46
+ CHANNEL_OPEN_FAILURE = 92
47
+ CHANNEL_WINDOW_ADJUST = 93
48
+ CHANNEL_DATA = 94
49
+ CHANNEL_EXTENDED_DATA = 95
50
+ CHANNEL_EOF = 96
51
+ CHANNEL_CLOSE = 97
52
+ CHANNEL_REQUEST = 98
53
+ CHANNEL_SUCCESS = 99
54
+ CHANNEL_FAILURE = 100
55
+
56
+ PING = 192
57
+ PONG = 193
58
+
59
+ class Message
60
+ class << self
61
+ def message_type(type = nil)
62
+ return @message_type if type.nil?
63
+
64
+ @message_type = type
65
+ end
66
+
67
+ def field(name, type, **options)
68
+ fields << { name:, type:, **options }
69
+
70
+ attr_reader(name)
71
+ end
72
+
73
+ def fields
74
+ @fields ||= []
75
+ end
76
+
77
+ def parse(data)
78
+ reader = Transport::Reader.new(data)
79
+
80
+ wire_message_type = reader.byte
81
+
82
+ unless wire_message_type == message_type
83
+ raise ProtocolError, "Expected #{name}, got message type #{wire_message_type}"
84
+ end
85
+
86
+ values = {}
87
+ fields.each do |f|
88
+ values[f[:name]] = read_field(reader, f)
89
+ end
90
+
91
+ new(**values)
92
+ end
93
+
94
+ private
95
+
96
+ def read_field(reader, field)
97
+ case field[:type]
98
+ when :raw then reader.read(field[:length])
99
+ when :remaining then reader.remaining
100
+ else
101
+ reader.send(field[:type])
102
+ end
103
+ end
104
+ end
105
+
106
+ def initialize(**values)
107
+ self.class.fields.each do |f|
108
+ value = if values.key?(f[:name])
109
+ values[f[:name]]
110
+ elsif f.key?(:default)
111
+ default = f[:default]
112
+ default.is_a?(Proc) ? default.call : default
113
+ else
114
+ raise ArgumentError, "missing keyword: :#{f[:name]}"
115
+ end
116
+
117
+ instance_variable_set(:"@#{f[:name]}", value)
118
+ end
119
+ end
120
+
121
+ def serialize
122
+ writer = Transport::Writer.new
123
+ writer.byte(self.class.message_type)
124
+
125
+ self.class.fields.each do |field|
126
+ value = instance_variable_get(:"@#{field[:name]}")
127
+
128
+ writer.send(field[:type], value)
129
+ end
130
+
131
+ writer.to_s
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ class AuthHandler
6
+ include Auth::DSL
7
+
8
+ def initialize(block)
9
+ @block = block
10
+ end
11
+
12
+ def call(*args)
13
+ result = instance_exec(*args, &@block)
14
+ Auth.normalize(result)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ class Config
6
+ MIN_PACKET_SIZE = 1024
7
+ MAX_PACKET_SIZE = 256 * 1024
8
+ DEFAULT_PACKET_SIZE = 32_768
9
+ DEFAULT_WINDOW_SIZE = 2 * 1024 * 1024
10
+
11
+ def initialize
12
+ @host = "127.0.0.1"
13
+ @port = 22
14
+ @nodelay = false
15
+ @server_id = SshId.new("Crussh_#{VERSION}")
16
+
17
+ @host_keys = []
18
+ @host_key_files = []
19
+ @preferred = Preferred.new
20
+
21
+ @limits = Limits.new
22
+ @max_packet_size = DEFAULT_PACKET_SIZE
23
+ @window_size = DEFAULT_WINDOW_SIZE
24
+ @channel_buffer_size = 10
25
+
26
+ @max_auth_attempts = 6
27
+ @auth_rejection_time = 1
28
+ @auth_rejection_time_initial = nil
29
+
30
+ @connection_timeout = 10
31
+ @auth_timeout = nil
32
+ @inactivity_timeout = nil
33
+
34
+ @keepalive_interval = nil
35
+ @keepalive_max = 3
36
+
37
+ @max_connections = nil
38
+ @max_unauthenticated = nil
39
+ end
40
+
41
+ attr_accessor :host,
42
+ :port,
43
+ :server_id,
44
+ :host_keys,
45
+ :host_key_files,
46
+ :preferred,
47
+ :limits,
48
+ :max_packet_size,
49
+ :window_size,
50
+ :channel_buffer_size,
51
+ :max_auth_attempts,
52
+ :auth_rejection_time,
53
+ :auth_rejection_time_initial,
54
+ :connection_timeout,
55
+ :auth_timeout,
56
+ :inactivity_timeout,
57
+ :keepalive_interval,
58
+ :keepalive_max,
59
+ :max_connections,
60
+ :max_unauthenticated
61
+
62
+ def generate_host_keys!
63
+ @host_keys << Keys.generate
64
+ self
65
+ end
66
+
67
+ def dup
68
+ copy = super
69
+ copy.instance_variable_set(:@limits, @limits.dup)
70
+ copy.instance_variable_set(:@host_keys, @host_keys.dup)
71
+ copy.instance_variable_set(:@host_key_files, @host_key_files.dup)
72
+ copy.instance_variable_set(:@preferred, @preferred.dup)
73
+ copy
74
+ end
75
+
76
+ def validate!
77
+ load_host_key_files!
78
+
79
+ validate_host!
80
+ validate_packet_size!
81
+ validate_timeouts!
82
+ validate_limits!
83
+
84
+ self
85
+ end
86
+
87
+ def nodelay? = @nodelay
88
+
89
+ private
90
+
91
+ def load_host_key_files!
92
+ @host_key_files.each do |path|
93
+ @host_keys << Keys.from_file(path)
94
+ end
95
+ end
96
+
97
+ def validate_host!
98
+ raise ConfigError, "No host keys configured" if @host_keys.empty?
99
+ raise ConfigError, "host is required" if @host.nil? || @host.empty?
100
+ raise ConfigError, "port must be between 1 and 65535" unless (1..65535).cover?(@port)
101
+ end
102
+
103
+ def validate_packet_size!
104
+ if @max_packet_size < MIN_PACKET_SIZE
105
+ raise ConfigError, "max_packet_size too small (min: #{MIN_PACKET_SIZE})"
106
+ end
107
+
108
+ if @max_packet_size > MAX_PACKET_SIZE
109
+ raise ConfigError, "max_packet_size too large (max: #{MAX_PACKET_SIZE})"
110
+ end
111
+
112
+ raise ConfigError, "window_size must be positive" if @window_size <= 0
113
+ raise ConfigError, "channel_buffer_size must be positive" if @channel_buffer_size <= 0
114
+ end
115
+
116
+ def validate_timeouts!
117
+ if @connection_timeout && @connection_timeout <= 0
118
+ raise ConfigError, "connection_timeout must be positive"
119
+ end
120
+
121
+ if @auth_timeout && @auth_timeout <= 0
122
+ raise ConfigError, "auth_timeout must be positive"
123
+ end
124
+
125
+ if @inactivity_timeout && @inactivity_timeout <= 0
126
+ raise ConfigError, "inactivity_timeout must be positive"
127
+ end
128
+
129
+ if @keepalive_interval && @keepalive_interval <= 0
130
+ raise ConfigError, "keepalive_interval must be positive"
131
+ end
132
+
133
+ raise ConfigError, "keepalive_max must be positive" if @keepalive_max <= 0
134
+
135
+ if @auth_rejection_time&.negative?
136
+ raise ConfigError, "auth_rejection_time cannot be negative"
137
+ end
138
+
139
+ if @auth_rejection_time_initial&.negative?
140
+ raise ConfigError, "auth_rejection_time_initial cannot be negative"
141
+ end
142
+ end
143
+
144
+ def validate_limits!
145
+ if @max_connections && @max_connections <= 0
146
+ raise ConfigError, "max_connections must be positive"
147
+ end
148
+
149
+ if @max_unauthenticated && @max_unauthenticated <= 0
150
+ raise ConfigError, "max_unauthenticated must be positive"
151
+ end
152
+
153
+ raise ConfigError, "max_auth_attempts must be positive" if @max_auth_attempts <= 0
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ module Layers
6
+ class Connection
7
+ def initialize(session)
8
+ @session = session
9
+ @channels = {}
10
+ @next_channel_id = 0
11
+ end
12
+
13
+ def run(task: Async::Task.current)
14
+ loop do
15
+ packet = read_with_timeout
16
+
17
+ break if packet.nil?
18
+
19
+ dispatch(packet)
20
+ end
21
+ rescue IOError, Errno::ECONNRESET, Crussh::ConnectionClosed => e
22
+ Logger.debug(self, "Connection closed", reason: e.class.name)
23
+ end
24
+
25
+ private
26
+
27
+ def config = @session.config
28
+ def server = @session.server
29
+ def packet_stream = @session.packet_stream
30
+
31
+ def read_with_timeout(task: Async::Task.current)
32
+ return @session.read_packet if config.inactivity_timeout.nil?
33
+
34
+ task.with_timeout(config.inactivity_timeout) do
35
+ @session.read_packet
36
+ end
37
+ rescue Async::TimeoutError
38
+ nil
39
+ end
40
+
41
+ def dispatch(packet)
42
+ message_type = packet.getbyte(0)
43
+
44
+ case message_type
45
+ when Protocol::CHANNEL_OPEN
46
+ channel_open(packet)
47
+ when Protocol::CHANNEL_DATA
48
+ channel_data(packet)
49
+ when Protocol::CHANNEL_EXTENDED_DATA
50
+ channel_extended_data(packet)
51
+ when Protocol::CHANNEL_EOF
52
+ channel_eof(packet)
53
+ when Protocol::CHANNEL_CLOSE
54
+ channel_close(packet)
55
+ when Protocol::CHANNEL_REQUEST
56
+ channel_request(packet)
57
+ when Protocol::CHANNEL_WINDOW_ADJUST
58
+ window_adjust(packet)
59
+ when Protocol::GLOBAL_REQUEST
60
+ global_request(packet)
61
+ when Protocol::DISCONNECT
62
+ disconnect(packet)
63
+ raise ConnectionClosed, "Client disconnected"
64
+ else
65
+ Logger.warn(self, "Unhandled message type", type: message_type)
66
+ message = Protocol::Unimplemented.new(sequence_number: @session.last_read_sequence)
67
+ @session.write_packet(message)
68
+ end
69
+ end
70
+
71
+ def channel_open(packet, task: Async::Task.current)
72
+ message = Protocol::ChannelOpen.parse(packet)
73
+ channel_type = message.channel_type.to_sym
74
+
75
+ unless server.accepts_channel?(channel_type)
76
+ send_channel_open_failure(message.sender_channel, :unknown_channel_type)
77
+ return
78
+ end
79
+
80
+ channel = create_channel(remote_id: message.sender_channel, window_size: message.initial_window_size, max_packet_size: message.maximum_packet_size)
81
+
82
+ if channel.nil?
83
+ send_channel_open_failure(message.sender_channel, :resource_shortage)
84
+ return
85
+ end
86
+
87
+ target = case message.channel_type
88
+ when "direct-tcpip"
89
+ message.direct_tcpip
90
+ when "forwarded-tcpip"
91
+ message.forwarded_tcpip
92
+ when "x11"
93
+ message.x11
94
+ end
95
+
96
+ unless server.open_channel?(channel_type, channel, target)
97
+ send_channel_open_failure(message.sender_channel, :administratively_prohibited)
98
+ @channels.delete(channel.id)
99
+ return
100
+ end
101
+
102
+ send_channel_open_confirmation(channel, message.sender_channel)
103
+
104
+ run_channel(channel_type, channel, target)
105
+ end
106
+
107
+ def create_channel(remote_id:, window_size:, max_packet_size:)
108
+ id = @next_channel_id
109
+ @next_channel_id += 1
110
+
111
+ channel = Channel.new(
112
+ session: @session,
113
+ id: id,
114
+ remote_id: remote_id,
115
+ remote_window_size: window_size,
116
+ local_window_size: config.window_size,
117
+ max_packet_size: [max_packet_size, config.max_packet_size].min,
118
+ buffer_size: config.channel_buffer_size,
119
+ )
120
+
121
+ @channels[id] = channel
122
+ channel
123
+ end
124
+
125
+ def run_channel(channel_type, channel, target)
126
+ case channel_type
127
+ when :session
128
+ nil
129
+ when :direct_tcpip
130
+ server.direct_tcpip(channel, target)
131
+ when :forwarded_tcpip
132
+ server.forwarded_tcpip(channel, target)
133
+ when :x11
134
+ server.x11(channel, target)
135
+ end
136
+ rescue => e
137
+ Logger.error(self, "Channel handler error", e)
138
+ end
139
+
140
+ def channel_data(packet)
141
+ message = Protocol::ChannelData.parse(packet)
142
+ channel = @channels[message.recipient_channel]
143
+ return if channel.nil?
144
+
145
+ channel.push_event(Channel::Data.new(data: message.data))
146
+ end
147
+
148
+ def channel_extended_data(packet)
149
+ message = Protocol::ChannelExtendedData.parse(packet)
150
+ channel = @channels[message.recipient_channel]
151
+ return if channel.nil?
152
+
153
+ channel.push_event(Channel::ExtendedData.new(data: message.data, type: message.data_type_code))
154
+ end
155
+
156
+ def channel_eof(packet)
157
+ message = Protocol::ChannelEof.parse(packet)
158
+ channel = @channels[message.recipient_channel]
159
+ return if channel.nil?
160
+
161
+ channel.push_event(Channel::EOF.new)
162
+ server.channel_eof(channel) if server.respond_to?(:channel_eof)
163
+ end
164
+
165
+ def channel_close(packet)
166
+ message = Protocol::ChannelClose.parse(packet)
167
+ channel = @channels[message.recipient_channel]
168
+ return if channel.nil?
169
+
170
+ channel.close unless channel.closed?
171
+ @channels.delete(channel.id)
172
+ channel.push_event(Channel::Closed.new)
173
+ server.channel_eof(channel) if server.respond_to?(:channel_eof)
174
+ end
175
+
176
+ def window_adjust(packet)
177
+ message = Protocol::ChannelWindowAdjust.parse(packet)
178
+ channel = @channels[message.recipient_channel]
179
+ return if channel.nil?
180
+
181
+ channel.adjust_remote_window(message.bytes_to_add)
182
+ end
183
+
184
+ def channel_request(packet)
185
+ message = Protocol::ChannelRequest.parse(packet)
186
+ channel = @channels[message.recipient_channel]
187
+
188
+ return if channel.nil?
189
+
190
+ accepted = case message.request_type
191
+ when "pty-req"
192
+ pty_request(channel, message)
193
+ when "env"
194
+ env_request(channel, message)
195
+ when "shell"
196
+ shell_request(channel)
197
+ when "exec"
198
+ exec_request(channel, message)
199
+ when "subsystem"
200
+ subsystem_request(channel, message)
201
+ when "window-change"
202
+ window_change(channel, message)
203
+ true
204
+ when "signal"
205
+ signal(channel, message)
206
+ true
207
+ when "x11-req"
208
+ x11_request(channel, message)
209
+ when "auth-agent-req@openssh.com"
210
+ agent_request(channel)
211
+ else
212
+ Logger.warn(self, "Unknown channel request", type: message.request_type)
213
+ false
214
+ end
215
+
216
+ return unless message.want_reply?
217
+
218
+ message = if accepted
219
+ Protocol::ChannelSuccess.new(recipient_channel: channel.remote_id)
220
+ else
221
+ Protocol::ChannelFailure.new(recipient_channel: channel.remote_id)
222
+ end
223
+
224
+ @session.write_packet(message)
225
+ end
226
+
227
+ def pty_request(channel, message)
228
+ pty = message.pty
229
+
230
+ accepted = server.accepts_request?(:pty, channel, term: pty.term, width: pty.width, height: pty.height, pixel_width: pty.pixel_width, pixel_height: pty.pixel_height, modes: pty.modes)
231
+
232
+ channel.pty = pty if accepted
233
+
234
+ accepted
235
+ end
236
+
237
+ def env_request(channel, message)
238
+ env = message.env
239
+
240
+ accepted = server.accepts_request?(:env, channel, name: env.variable_name, value: env.variable_value)
241
+
242
+ channel.set_env(env.variable_name, env.variable_value) if accepted
243
+
244
+ accepted
245
+ end
246
+
247
+ def shell_request(channel)
248
+ return false unless server.has_handler?(:shell)
249
+
250
+ Async do
251
+ server.dispatch_handler(:shell, channel, @session)
252
+ end
253
+
254
+ true
255
+ end
256
+
257
+ def exec_request(channel, message)
258
+ return false unless server.has_handler?(:exec)
259
+
260
+ Async do
261
+ server.dispatch_handler(:exec, channel, @session, message.command)
262
+ end
263
+
264
+ true
265
+ end
266
+
267
+ def subsystem_request(channel, message)
268
+ return false unless server.has_handler?(:subsystem)
269
+
270
+ Async do
271
+ server.dispatch_handler(:subsystem, channel, @session, message.subsystem_name)
272
+ end
273
+
274
+ true
275
+ end
276
+
277
+ def window_change(channel, message)
278
+ window_change = message.window_change
279
+
280
+ channel.update_window(window_change)
281
+ end
282
+
283
+ def signal(channel, message)
284
+ channel.push_event(message.signal)
285
+ end
286
+
287
+ def x11_request(channel, message)
288
+ x11 = message.x11
289
+
290
+ server.accepts_request?(:x11, channel, single_connection: x11.single_connection, protocol: x11.auth_protocol, cookie: x11.auth_cookie, screen: x11.screen_number)
291
+ end
292
+
293
+ def agent_request(channel)
294
+ server.accepts_request?(:agent, channel)
295
+ end
296
+
297
+ def global_request(packet)
298
+ message = Protocol::GlobalRequest.parse(packet)
299
+
300
+ accepted = case message.request_type
301
+ when Heartbeat::KEEPALIVE_REQUEST
302
+ true
303
+ when "tcpip-forward"
304
+ tcpip_forward = message.tcpip_forward
305
+
306
+ server.respond_to?(:tcpip_forward?) && server.tcpip_forward?(tcpip_forward.address, tcpip_forward.port)
307
+ when "cancel-tcpip-forward"
308
+ tcpip_forward = message.tcpip_forward
309
+
310
+ server.respond_to?(:cancel_tcpip_forward?) && server.cancel_tcpip_forward?(tcpip_forward.address, tcpip_forward.port)
311
+ end
312
+
313
+ return unless message.want_reply?
314
+
315
+ message = if accepted
316
+ Protocol::RequestSuccess.new(response_data: "")
317
+ else
318
+ Protocol::RequestFailure.new
319
+ end
320
+
321
+ @session.write_packet(message)
322
+ end
323
+
324
+ def disconnect(packet)
325
+ message = Protocol::Disconnect.parse(packet)
326
+
327
+ Logger.info(self, "Client disconnected", reason: message.reason_code, description: message.description)
328
+ end
329
+
330
+ def send_channel_open_confirmation(channel, recipient_channel)
331
+ message = Protocol::ChannelOpenConfirmation.new(
332
+ recipient_channel:,
333
+ sender_channel: channel.id,
334
+ initial_window_size: config.window_size,
335
+ maximum_packet_size: config.max_packet_size,
336
+ )
337
+
338
+ @session.write_packet(message)
339
+ end
340
+
341
+ REASON_MAP = {
342
+ administratively_prohibited: 1,
343
+ connect_failed: 2,
344
+ unknown_channel_type: 3,
345
+ resource_shortage: 4,
346
+ }
347
+ DESCRIPTION_MAP = {
348
+ administratively_prohibited: "Administratively Prohibited",
349
+ connect_failed: "Connect failed",
350
+ unknown_channel_type: "Unknown channel type",
351
+ resource_shortage: "No more resources, sorry :(",
352
+ }
353
+ def send_channel_open_failure(recipient_channel, reason)
354
+ reason_code = REASON_MAP[reason]
355
+ description = DESCRIPTION_MAP[reason]
356
+
357
+ message = Protocol::ChannelOpenFailure.new(recipient_channel:, reason_code:, description:)
358
+ @session.write_packet(message)
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end