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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +371 -0
- data/ext/poly1305/Cargo.toml +13 -0
- data/ext/poly1305/extconf.rb +6 -0
- data/ext/poly1305/src/lib.rs +75 -0
- data/lib/crussh/auth.rb +46 -0
- data/lib/crussh/channel/key_parser.rb +125 -0
- data/lib/crussh/channel.rb +381 -0
- data/lib/crussh/cipher/algorithm.rb +31 -0
- data/lib/crussh/cipher/chacha20poly1305.rb +98 -0
- data/lib/crussh/cipher.rb +25 -0
- data/lib/crussh/compression.rb +42 -0
- data/lib/crussh/gatekeeper.rb +50 -0
- data/lib/crussh/handler/line_buffer.rb +131 -0
- data/lib/crussh/handler.rb +128 -0
- data/lib/crussh/heartbeat.rb +68 -0
- data/lib/crussh/kex/algorithm.rb +86 -0
- data/lib/crussh/kex/curve25519.rb +30 -0
- data/lib/crussh/kex/exchange.rb +234 -0
- data/lib/crussh/kex.rb +42 -0
- data/lib/crussh/keys/key_pair.rb +61 -0
- data/lib/crussh/keys/public_key.rb +35 -0
- data/lib/crussh/keys.rb +70 -0
- data/lib/crussh/limits.rb +45 -0
- data/lib/crussh/logger.rb +95 -0
- data/lib/crussh/mac/algorithm.rb +23 -0
- data/lib/crussh/mac/crypto.rb +60 -0
- data/lib/crussh/mac/none.rb +9 -0
- data/lib/crussh/mac.rb +28 -0
- data/lib/crussh/negotiator.rb +41 -0
- data/lib/crussh/preferred.rb +16 -0
- data/lib/crussh/protocol/channel_close.rb +11 -0
- data/lib/crussh/protocol/channel_data.rb +12 -0
- data/lib/crussh/protocol/channel_eof.rb +11 -0
- data/lib/crussh/protocol/channel_extended_data.rb +13 -0
- data/lib/crussh/protocol/channel_failure.rb +11 -0
- data/lib/crussh/protocol/channel_open.rb +69 -0
- data/lib/crussh/protocol/channel_open_confirmation.rb +15 -0
- data/lib/crussh/protocol/channel_open_failure.rb +14 -0
- data/lib/crussh/protocol/channel_request.rb +146 -0
- data/lib/crussh/protocol/channel_success.rb +11 -0
- data/lib/crussh/protocol/channel_window_adjust.rb +12 -0
- data/lib/crussh/protocol/debug.rb +15 -0
- data/lib/crussh/protocol/disconnect.rb +39 -0
- data/lib/crussh/protocol/ext_info.rb +48 -0
- data/lib/crussh/protocol/global_request.rb +46 -0
- data/lib/crussh/protocol/ignore.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_init.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_reply.rb +13 -0
- data/lib/crussh/protocol/kex_init.rb +38 -0
- data/lib/crussh/protocol/new_keys.rb +9 -0
- data/lib/crussh/protocol/ping.rb +11 -0
- data/lib/crussh/protocol/pong.rb +11 -0
- data/lib/crussh/protocol/request_failure.rb +9 -0
- data/lib/crussh/protocol/request_success.rb +11 -0
- data/lib/crussh/protocol/service_accept.rb +11 -0
- data/lib/crussh/protocol/service_request.rb +11 -0
- data/lib/crussh/protocol/unimplemented.rb +11 -0
- data/lib/crussh/protocol/userauth_banner.rb +12 -0
- data/lib/crussh/protocol/userauth_failure.rb +12 -0
- data/lib/crussh/protocol/userauth_pk_ok.rb +12 -0
- data/lib/crussh/protocol/userauth_request.rb +52 -0
- data/lib/crussh/protocol/userauth_success.rb +9 -0
- data/lib/crussh/protocol.rb +135 -0
- data/lib/crussh/server/auth_handler.rb +18 -0
- data/lib/crussh/server/config.rb +157 -0
- data/lib/crussh/server/layers/connection.rb +363 -0
- data/lib/crussh/server/layers/transport.rb +49 -0
- data/lib/crussh/server/layers/userauth.rb +232 -0
- data/lib/crussh/server/request_rule.rb +76 -0
- data/lib/crussh/server/session.rb +192 -0
- data/lib/crussh/server.rb +214 -0
- data/lib/crussh/ssh_id.rb +44 -0
- data/lib/crussh/transport/packet_stream.rb +245 -0
- data/lib/crussh/transport/reader.rb +98 -0
- data/lib/crussh/transport/version_exchange.rb +26 -0
- data/lib/crussh/transport/writer.rb +72 -0
- data/lib/crussh/version.rb +5 -0
- data/lib/crussh.rb +61 -0
- data/sig/crussh.rbs +4 -0
- 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
|