ruby_smb 3.3.22 → 3.3.25

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.
@@ -0,0 +1,174 @@
1
+ RSpec.describe RubySMB::Gss::Provider::Multi do
2
+ let(:username) { 'RubySMB' }
3
+ let(:domain) { 'WORKGROUP' }
4
+ let(:password) { 'password' }
5
+ let(:ntlm_provider) do
6
+ RubySMB::Gss::Provider::NTLM.new.tap { |provider| provider.put_account(username, password, domain: domain) }
7
+ end
8
+ let(:other_authenticator) { double('authenticator', process: nil, reset!: nil, session_key: nil) }
9
+ # a stand-in for any non-NTLM mechanism, so the routing can be exercised without a second real provider
10
+ let(:other_provider) do
11
+ authenticator = other_authenticator
12
+ Class.new(RubySMB::Gss::Provider::Base) do
13
+ define_method(:mech_types) do
14
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5]
15
+ end
16
+
17
+ define_method(:new_authenticator) { |_server_client| authenticator }
18
+ end.new
19
+ end
20
+ let(:server_client) { double('server_client', logger: Logger.new(IO::NULL)) }
21
+
22
+ # referenced explicitly rather than via described_class, which resolves to the authenticator inside the nested group
23
+ subject(:provider) { RubySMB::Gss::Provider::Multi.new([other_provider, ntlm_provider]) }
24
+
25
+ describe '#initialize' do
26
+ it 'requires at least one provider' do
27
+ expect { RubySMB::Gss::Provider::Multi.new([]) }.to raise_error(ArgumentError)
28
+ expect { RubySMB::Gss::Provider::Multi.new(nil) }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ describe '#mech_types' do
33
+ it 'advertises every mechanism of every provider' do
34
+ expect(provider.mech_types.map(&:oid)).to eq(
35
+ [
36
+ RubySMB::Gss::OID_KERBEROS_5.oid,
37
+ RubySMB::Gss::OID_MICROSOFT_KERBEROS_5.oid,
38
+ RubySMB::Gss::OID_NTLMSSP.oid
39
+ ]
40
+ )
41
+ end
42
+
43
+ it 'preserves the order the providers were given in' do
44
+ reversed = RubySMB::Gss::Provider::Multi.new([ntlm_provider, other_provider])
45
+ expect(reversed.mech_types.first.oid).to eq(RubySMB::Gss::OID_NTLMSSP.oid)
46
+ end
47
+
48
+ it 'advertises a mechanism supported by two providers only once' do
49
+ duplicated = RubySMB::Gss::Provider::Multi.new([ntlm_provider, RubySMB::Gss::Provider::NTLM.new])
50
+ expect(duplicated.mech_types.length).to eq(1)
51
+ end
52
+ end
53
+
54
+ describe '#provider_for' do
55
+ it 'finds the provider that handles the mechanism' do
56
+ expect(provider.provider_for(RubySMB::Gss::OID_KERBEROS_5)).to be(other_provider)
57
+ expect(provider.provider_for(RubySMB::Gss::OID_NTLMSSP)).to be(ntlm_provider)
58
+ end
59
+
60
+ it 'is nil when no provider handles the mechanism' do
61
+ expect(provider.provider_for(RubySMB::Gss::OID_NEGOEX)).to be_nil
62
+ end
63
+ end
64
+
65
+ describe RubySMB::Gss::Provider::Multi::Authenticator do
66
+ subject(:authenticator) { provider.new_authenticator(server_client) }
67
+
68
+ describe '#process' do
69
+ context 'when building the advertisement' do
70
+ it 'offers all of the mechanisms' do
71
+ buffer = authenticator.process(nil).buffer
72
+ expect(buffer).to eq(RubySMB::Gss.gss_neg_token_init(provider.mech_types))
73
+ end
74
+
75
+ it 'matches the underlying provider when only one is held' do
76
+ single = described_class.new(RubySMB::Gss::Provider::Multi.new([ntlm_provider]), server_client)
77
+ expect(single.process(nil).buffer).to eq(ntlm_provider.new_authenticator(server_client).process(nil).buffer)
78
+ end
79
+
80
+ it 'succeeds' do
81
+ expect(authenticator.process(nil).nt_status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
82
+ end
83
+ end
84
+
85
+ context 'when the client selects a mechanism' do
86
+ it 'routes the token to the provider that handles it' do
87
+ expect(other_authenticator).to receive(:process)
88
+ authenticator.process(gss_init(RubySMB::Gss::OID_KERBEROS_5))
89
+ end
90
+
91
+ it 'routes an NTLM token to the NTLM provider' do
92
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
93
+ result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
94
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED)
95
+ end
96
+
97
+ it 'refuses a mechanism no provider handles' do
98
+ expect(authenticator.process(gss_init(RubySMB::Gss::OID_NEGOEX))).to be_nil
99
+ end
100
+ end
101
+
102
+ context 'when the client continues an exchange' do
103
+ it 'refuses a continuation before a mechanism has been selected' do
104
+ # a NegTokenResp carries no mechanism OID, so there is nothing to route on
105
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
106
+ end
107
+ end
108
+
109
+ it 'returns nil for a malformed request' do
110
+ expect(authenticator.process('not asn1 at all')).to be_nil
111
+ end
112
+ end
113
+
114
+ describe 'a complete NTLM exchange' do
115
+ it 'authenticates the same as the NTLM provider on its own' do
116
+ expect(complete_ntlm_exchange(authenticator)).to eq(
117
+ complete_ntlm_exchange(ntlm_provider.new_authenticator(server_client))
118
+ )
119
+ end
120
+
121
+ it 'succeeds for a known account' do
122
+ status, identity = complete_ntlm_exchange(authenticator)
123
+ expect(status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
124
+ expect(identity).to eq("#{domain}\\#{username}")
125
+ end
126
+
127
+ it 'exposes the session key of the mechanism that authenticated' do
128
+ complete_ntlm_exchange(authenticator)
129
+ expect(authenticator.session_key).to_not be_nil
130
+ end
131
+ end
132
+
133
+ describe '#reset!' do
134
+ it 'forgets the selected mechanism' do
135
+ complete_ntlm_exchange(authenticator)
136
+ authenticator.reset!
137
+ expect(authenticator.session_key).to be_nil
138
+ # with no mechanism selected, a continuation token has nothing to route to
139
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
140
+ end
141
+ end
142
+ end
143
+
144
+ # Build a NegTokenInit that selects the specified mechanism, with an empty mechToken.
145
+ def gss_init(mech_type)
146
+ OpenSSL::ASN1::ASN1Data.new(
147
+ [
148
+ RubySMB::Gss::OID_SPNEGO,
149
+ OpenSSL::ASN1::ASN1Data.new(
150
+ [
151
+ OpenSSL::ASN1::Sequence.new(
152
+ [
153
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([mech_type])], 0, :CONTEXT_SPECIFIC),
154
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new('')], 2, :CONTEXT_SPECIFIC)
155
+ ]
156
+ )
157
+ ], 0, :CONTEXT_SPECIFIC
158
+ )
159
+ ], 0, :APPLICATION
160
+ ).to_der
161
+ end
162
+
163
+ # Drive a full NTLM negotiation through the authenticator, returning the final status and identity.
164
+ def complete_ntlm_exchange(authenticator)
165
+ authenticator.process(nil)
166
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
167
+ challenge_result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
168
+ raw_type2 = RubySMB::Gss.asn1dig(OpenSSL::ASN1.decode(challenge_result.buffer), 0, 2, 0).value
169
+ type2 = Net::NTLM::Message.parse(raw_type2)
170
+ type3 = type2.response({ user: username, password: password, domain: domain }, { ntlmv2: true })
171
+ result = authenticator.process(RubySMB::Gss.gss_type3(type3.serialize))
172
+ [result.nt_status, result.identity]
173
+ end
174
+ end
@@ -65,6 +65,19 @@ RSpec.describe RubySMB::Gss::Provider::NTLM::Authenticator do
65
65
  it 'should process a NTLM type 1 message and return a type2 message' do
66
66
  expect(authenticator.process_ntlm_type1(type1_msg)).to be_a Net::NTLM::Message::Type2
67
67
  end
68
+
69
+ it 'includes the server names in the serialized target info' do
70
+ type2_msg = Net::NTLM::Message.parse(authenticator.process_ntlm_type1(type1_msg).serialize)
71
+ target_info = Net::NTLM::TargetInfo.new(type2_msg.target_info)
72
+
73
+ expect(target_info.av_pairs).to include(
74
+ Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => provider.netbios_domain.encode('UTF-16LE').b,
75
+ Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => provider.netbios_hostname.encode('UTF-16LE').b,
76
+ Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => provider.dns_domain.encode('UTF-16LE').b,
77
+ Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => provider.dns_hostname.encode('UTF-16LE').b
78
+ )
79
+ expect(target_info.av_pairs[Net::NTLM::TargetInfo::MSV_AV_TIMESTAMP].bytesize).to eq 8
80
+ end
68
81
  end
69
82
 
70
83
  describe '#process_ntlm_type3' do
@@ -0,0 +1,80 @@
1
+ RSpec.describe 'SPNEGO negotiation tokens' do
2
+ let(:mech_types) do
3
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5, RubySMB::Gss::OID_NTLMSSP]
4
+ end
5
+
6
+ describe RubySMB::Gss::SpnegoNegTokenInit do
7
+ describe '.build' do
8
+ subject(:token) { described_class.build(mech_types) }
9
+
10
+ # the exact bytes a server advertised before this was modelled with RASN1, kept so the wire format does not
11
+ # drift: the SPNEGO OID, the three mechanisms, and the Microsoft negHints placeholder
12
+ let(:legacy_der) do
13
+ [
14
+ '605e06062b0601050502a0543052a024302206092a864886f71201020206092a864882' \
15
+ 'f712010202060a2b06010401823702020aa32a3028a0261b246e6f745f646566696e65' \
16
+ '645f696e5f5246433431373840706c656173655f69676e6f7265'
17
+ ].pack('H*')
18
+ end
19
+
20
+ it 'is byte-identical to the token the hand-rolled builder produced' do
21
+ expect(token).to eq(legacy_der)
22
+ end
23
+
24
+ # the exact NTLM-only advertisement a default server built before this change, when the NTLM provider
25
+ # hardcoded a single OID_NTLMSSP. existing servers still emit this, so lock it against a wire regression.
26
+ it 'is byte-identical to the NTLM-only advertisement a default server built before this change' do
27
+ legacy_ntlm_der = [
28
+ '604806062b0601050502a03e303ca00e300c060a2b06010401823702020aa32a3028' \
29
+ 'a0261b246e6f745f646566696e65645f696e5f5246433431373840706c656173655f' \
30
+ '69676e6f7265'
31
+ ].pack('H*')
32
+ expect(described_class.build([RubySMB::Gss::OID_NTLMSSP])).to eq(legacy_ntlm_der)
33
+ end
34
+
35
+ it 'advertises the mechanisms in order' do
36
+ decoded = OpenSSL::ASN1.decode(token)
37
+ mech_list = decoded.value[1].value[0].value[0].value[0].value
38
+ expect(mech_list.map(&:oid)).to eq(mech_types.map(&:oid))
39
+ end
40
+
41
+ it 'carries the Microsoft negHints placeholder' do
42
+ decoded = OpenSSL::ASN1.decode(token)
43
+ hint = decoded.value[1].value[0].value[1].value[0].value[0].value[0].value
44
+ expect(hint).to eq(RubySMB::Gss::SpnegoNegTokenInit::NEG_HINTS_NAME)
45
+ end
46
+ end
47
+
48
+ describe '.parse' do
49
+ # a SPNEGO NegTokenInit selecting Kerberos and carrying the given mechanism token
50
+ def neg_token_init(token)
51
+ inner = [OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([RubySMB::Gss::OID_KERBEROS_5])], 0, :CONTEXT_SPECIFIC)]
52
+ inner << OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new(token)], 2, :CONTEXT_SPECIFIC) unless token.nil?
53
+
54
+ OpenSSL::ASN1::ASN1Data.new(
55
+ [
56
+ RubySMB::Gss::OID_SPNEGO,
57
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new(inner)], 0, :CONTEXT_SPECIFIC)
58
+ ], 0, :APPLICATION
59
+ ).to_der
60
+ end
61
+
62
+ it 'reads the mechanism token' do
63
+ expect(described_class.parse(neg_token_init('a mechanism token')).mech_token).to eq('a mechanism token')
64
+ end
65
+
66
+ it 'is nil when the token carries no mechanism token' do
67
+ expect(described_class.parse(neg_token_init(nil)).mech_token).to be_nil
68
+ end
69
+ end
70
+ end
71
+
72
+ describe RubySMB::Gss::SpnegoNegTokenTarg do
73
+ describe '.parse' do
74
+ it 'reads the response token from a continuation' do
75
+ targ = described_class.parse(RubySMB::Gss.gss_type3('a continuation token'))
76
+ expect(targ.response_token).to eq('a continuation token')
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatusRequest do
4
+ subject(:request) { described_class.new(transaction_id: 0x1234) }
5
+
6
+ describe 'encoded bytes' do
7
+ let(:bytes) { request.to_binary_s }
8
+
9
+ it 'starts with a 12-byte NBNS header' do
10
+ expect(bytes[0, 2].unpack1('n')).to eq(0x1234)
11
+ expect(bytes[2, 2].unpack1('n')).to eq(0x0000) # flags
12
+ expect(bytes[4, 2].unpack1('n')).to eq(1) # qdcount
13
+ expect(bytes[6, 2].unpack1('n')).to eq(0) # ancount
14
+ expect(bytes[8, 2].unpack1('n')).to eq(0) # nscount
15
+ expect(bytes[10, 2].unpack1('n')).to eq(0) # arcount
16
+ end
17
+
18
+ it 'encodes the wildcard question name as 34 bytes (length + 32-char L1 + null)' do
19
+ expect(bytes[12].unpack1('C')).to eq(0x20) # label length
20
+ expect(bytes[13, 32]).to eq('CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
21
+ expect(bytes[45].unpack1('C')).to eq(0x00) # null label terminator
22
+ end
23
+
24
+ it 'ends with QTYPE=NBSTAT and QCLASS=IN' do
25
+ expect(bytes[46, 2].unpack1('n')).to eq(described_class::QUESTION_TYPE_NBSTAT)
26
+ expect(bytes[48, 2].unpack1('n')).to eq(described_class::QUESTION_CLASS_IN)
27
+ end
28
+
29
+ it 'is exactly 50 bytes long' do
30
+ expect(bytes.bytesize).to eq(50)
31
+ end
32
+ end
33
+
34
+ describe 'flags' do
35
+ it 'packs the OPCODE, NM_FLAGS and RCODE fields in RFC bit order' do
36
+ request.response = 1
37
+ request.nm_flags.authoritative_answer = 1
38
+ request.nm_flags.broadcast = 1
39
+ request.rcode = 0x5
40
+
41
+ expect(request.to_binary_s[2, 2].unpack1('n')).to eq(0x8415)
42
+ end
43
+
44
+ it 'defaults every request flag bit to zero' do
45
+ expect(request.to_binary_s[2, 2].unpack1('n')).to eq(0x0000)
46
+ expect(request.response.to_i).to eq(0)
47
+ expect(request.opcode.to_i).to eq(0)
48
+ expect(request.nm_flags.broadcast.to_i).to eq(0)
49
+ expect(request.rcode.to_i).to eq(0)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatusResponse do
4
+ def build_response(names)
5
+ data = ''.b
6
+ data << [0x1234].pack('n') # transaction_id
7
+ data << [0x8400].pack('n') # flags: response, authoritative
8
+ data << [0].pack('n') # qdcount
9
+ data << [1].pack('n') # ancount
10
+ data << [0].pack('n') << [0].pack('n') # nscount, arcount
11
+ data << [0x20].pack('C') << ('A' * 32) << "\x00".b # owner name L1
12
+ data << [0x0021].pack('n') # RR type NBSTAT
13
+ data << [0x0001].pack('n') # RR class IN
14
+ data << [0].pack('N') # TTL
15
+ data << [1 + names.length * 18 + 46].pack('n') # rdlength
16
+ data << [names.length].pack('C')
17
+ names.each do |name, suffix, flags|
18
+ data << name.to_s.ljust(15, ' ') << [suffix].pack('C') << [flags].pack('n')
19
+ end
20
+ data << ("\x00".b * 46) # statistics (unused)
21
+ data
22
+ end
23
+
24
+ describe 'parsing' do
25
+ it 'decodes the name table' do
26
+ response = described_class.read(build_response([
27
+ ['WIN95', 0x00, 0x0400],
28
+ ['WIN95', 0x20, 0x0400],
29
+ ['WORKGROUP', 0x00, 0x8400]
30
+ ]))
31
+ expect(response.num_names).to eq(3)
32
+ expect(response.node_names[0].netbios_name.to_s.rstrip).to eq('WIN95')
33
+ expect(response.node_names[0].suffix).to eq(0x00)
34
+ expect(response.node_names[1].suffix).to eq(0x20)
35
+ expect(response.node_names[2].group?).to be true
36
+ expect(response.response.to_i).to eq(1)
37
+ expect(response.nm_flags.authoritative_answer.to_i).to eq(1)
38
+ end
39
+ end
40
+
41
+ describe 'flags' do
42
+ it 'decodes the header OPCODE and NM_FLAGS fields from the second word' do
43
+ response = described_class.read(build_response([['WIN95', 0x20, 0x0400]]))
44
+
45
+ expect(response.response.to_i).to eq(1) # high bit of 0x8400
46
+ expect(response.nm_flags.authoritative_answer.to_i).to eq(1)
47
+ end
48
+
49
+ it 'defines NODE_NAME flags in RFC bit order' do
50
+ flags = RubySMB::Nbss::NodeStatusNameFlags.new
51
+ flags.group = 1
52
+ flags.active = 1
53
+
54
+ expect(flags.to_binary_s.unpack1('n')).to eq(0x8400)
55
+ expect(flags.group.to_i).to eq(1)
56
+ expect(flags.active.to_i).to eq(1)
57
+ end
58
+
59
+ it 'exposes parsed NODE_NAME flags as named fields' do
60
+ response = described_class.read(build_response([
61
+ ['WORKGROUP', 0x00, 0x8400],
62
+ ['FILESERVER', 0x20, 0x0400]
63
+ ]))
64
+
65
+ group_entry = response.node_names[0]
66
+ unique_entry = response.node_names[1]
67
+
68
+ expect(group_entry.name_flags.group.to_i).to eq(1)
69
+ expect(group_entry.name_flags.active.to_i).to eq(1)
70
+ expect(group_entry.group?).to be true
71
+ expect(unique_entry.name_flags.group.to_i).to eq(0)
72
+ expect(unique_entry.unique?).to be true
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatus do
4
+ let(:udp_sock) { double('UDPSocket') }
5
+ # #recvfrom returns the sender as [address_family, port, host, numeric_host];
6
+ # the numeric address at index 3 is matched against the queried target.
7
+ let(:peer) { ['AF_INET', 137, '10.0.0.2', '10.0.0.2'] }
8
+
9
+ # Node Status requests carry a random transaction ID; the response is only
10
+ # accepted when it echoes that ID back. Pin it so the fixtures match.
11
+ before { allow(SecureRandom).to receive(:random_number).and_return(0x1234) }
12
+
13
+ def build_response(names)
14
+ data = ''.b
15
+ data << [0x1234].pack('n') # transaction_id
16
+ data << [0x8400].pack('n') # flags
17
+ data << [0].pack('n') << [1].pack('n') # qdcount, ancount
18
+ data << [0].pack('n') << [0].pack('n') # nscount, arcount
19
+ data << [0x20].pack('C') << ('A' * 32) << "\x00".b # owner name
20
+ data << [0x0021].pack('n') << [0x0001].pack('n')
21
+ data << [0].pack('N') # TTL
22
+ data << [1 + names.length * 18 + 46].pack('n')
23
+ data << [names.length].pack('C')
24
+ names.each do |name, suffix, flags|
25
+ data << name.to_s.ljust(15, ' ') << [suffix].pack('C') << [flags].pack('n')
26
+ end
27
+ data << ("\x00".b * 46)
28
+ data
29
+ end
30
+
31
+ describe '.query' do
32
+ it 'sends the request with #send(mesg, flags, host, port) and reads via recvfrom(maxlen)' do
33
+ response_bytes = build_response([
34
+ ['WIN95', 0x00, 0x0400],
35
+ ['WIN95', 0x20, 0x0400],
36
+ ['WORKGROUP', 0x00, 0x8400]
37
+ ])
38
+
39
+ expect(udp_sock).to receive(:send) do |bytes, flags, host, port|
40
+ expect(flags).to eq(0)
41
+ expect(host).to eq('10.0.0.2')
42
+ expect(port).to eq(137)
43
+ expect(bytes.bytesize).to eq(50)
44
+ end
45
+ expect(IO).to receive(:select).and_return([udp_sock])
46
+ expect(udp_sock).to receive(:recvfrom).with(4096).and_return([response_bytes, peer])
47
+
48
+ entries = described_class.query('10.0.0.2', udp_socket: udp_sock)
49
+ expect(entries.size).to eq(3)
50
+ expect(entries[1].name).to eq('WIN95')
51
+ expect(entries[1].suffix).to eq(0x20)
52
+ expect(entries[1].unique?).to be true
53
+ expect(entries[2].group).to be true
54
+ end
55
+
56
+ it 'retries up to the configured limit before giving up' do
57
+ call_count = 0
58
+ allow(udp_sock).to receive(:send) { call_count += 1 }
59
+ allow(IO).to receive(:select).and_return(nil) # always time out
60
+
61
+ expect(described_class.query('10.0.0.2', retries: 4, timeout: 0.01, udp_socket: udp_sock)).to be_nil
62
+ expect(call_count).to eq(4)
63
+ end
64
+
65
+ it 'returns nil when the response can not be parsed' do
66
+ expect(udp_sock).to receive(:send)
67
+ expect(IO).to receive(:select).and_return([udp_sock])
68
+ expect(udp_sock).to receive(:recvfrom).and_return(["\xff\xff".b, peer])
69
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
70
+ end
71
+
72
+ it 'retries after a malformed datagram and succeeds on a later attempt' do
73
+ good = build_response([['WIN95', 0x20, 0x0400]])
74
+ allow(udp_sock).to receive(:send)
75
+ allow(IO).to receive(:select).and_return([udp_sock])
76
+ # First datagram is truncated (NodeStatusResponse.read raises); the second
77
+ # is well-formed. The rescue must stay inside the retry loop for this.
78
+ allow(udp_sock).to receive(:recvfrom).and_return(["\xff\xff".b, peer], [good, peer])
79
+
80
+ entries = described_class.query('10.0.0.2', retries: 3, timeout: 0.01, udp_socket: udp_sock)
81
+ expect(entries).not_to be_nil
82
+ expect(entries.first.suffix).to eq(0x20)
83
+ end
84
+
85
+ it 'raises ArgumentError when host is not a numeric IPv4 address' do
86
+ expect { described_class.query('example.com', udp_socket: udp_sock) }.to raise_error(ArgumentError)
87
+ end
88
+
89
+ it 'rejects a reply whose transaction ID does not match the request' do
90
+ allow(SecureRandom).to receive(:random_number).and_return(0x1234)
91
+ mismatched = build_response([['WIN95', 0x20, 0x0400]])
92
+ mismatched[0, 2] = [0x9999].pack('n') # overwrite transaction_id
93
+ expect(udp_sock).to receive(:send)
94
+ expect(IO).to receive(:select).and_return([udp_sock])
95
+ expect(udp_sock).to receive(:recvfrom).and_return([mismatched, peer])
96
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
97
+ end
98
+
99
+ it 'rejects a reply from an unexpected source address' do
100
+ spoofed = build_response([['WIN95', 0x20, 0x0400]])
101
+ expect(udp_sock).to receive(:send)
102
+ expect(IO).to receive(:select).and_return([udp_sock])
103
+ expect(udp_sock).to receive(:recvfrom).and_return([spoofed, ['AF_INET', 137, '10.0.0.9', '10.0.0.9']])
104
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
105
+ end
106
+
107
+ it 'returns nil on IOError and does not close the socket' do
108
+ allow(udp_sock).to receive(:send).and_raise(IOError, 'boom')
109
+ expect(udp_sock).not_to receive(:close)
110
+ expect(described_class.query('10.0.0.2', retries: 1, udp_socket: udp_sock)).to be_nil
111
+ end
112
+ end
113
+
114
+ describe '.file_server_name' do
115
+ it 'returns the unique 0x20 entry' do
116
+ response_bytes = build_response([
117
+ ['WORKGROUP', 0x00, 0x8400],
118
+ ['FILESERVER', 0x20, 0x0400]
119
+ ])
120
+ allow(udp_sock).to receive(:send)
121
+ allow(IO).to receive(:select).and_return([udp_sock])
122
+ allow(udp_sock).to receive(:recvfrom).and_return([response_bytes, peer])
123
+
124
+ expect(described_class.file_server_name('10.0.0.2', udp_socket: udp_sock)).to eq('FILESERVER')
125
+ end
126
+
127
+ it 'returns nil when no unique 0x20 entry is present' do
128
+ response_bytes = build_response([['HOST', 0x00, 0x0400]])
129
+ allow(udp_sock).to receive(:send)
130
+ allow(IO).to receive(:select).and_return([udp_sock])
131
+ allow(udp_sock).to receive(:recvfrom).and_return([response_bytes, peer])
132
+
133
+ expect(described_class.file_server_name('10.0.0.2', udp_socket: udp_sock)).to be_nil
134
+ end
135
+ end
136
+
137
+ describe RubySMB::Nbss::NodeStatus::Entry do
138
+ it '#to_s formats like nmblookup output' do
139
+ entry = described_class.new('WIN95', 0x20, false, true)
140
+ expect(entry.to_s).to include('WIN95')
141
+ expect(entry.to_s).to include('<20>')
142
+ expect(entry.to_s).to include('UNIQUE')
143
+ expect(entry.to_s).to include('ACTIVE')
144
+ end
145
+ end
146
+ end
@@ -8,7 +8,9 @@ RSpec.describe RubySMB::NTLM::Client::Session do
8
8
  AGwAbwBjAGEAbAADADgAVwBJAE4ALQAzAE0AUwBQADgASwAyAEwAQwBHAEMA
9
9
  LgBtAHMAZgBsAGEAYgAuAGwAbwBjAGEAbAAHAAgAS6UAWjxl2AEAAAAA
10
10
  }) }
11
- subject(:client) { RubySMB::NTLM::Client.new('rubysmb', 'rubysmb', flags: RubySMB::NTLM::DEFAULT_CLIENT_FLAGS) }
11
+ let(:username) { 'rubysmb' }
12
+ let(:password) { 'rubysmb' }
13
+ let(:client) { RubySMB::NTLM::Client.new(username, password, flags: RubySMB::NTLM::DEFAULT_CLIENT_FLAGS) }
12
14
  subject(:session) { described_class.new(client, message) }
13
15
 
14
16
  describe '#authenticate!' do
@@ -17,72 +19,49 @@ RSpec.describe RubySMB::NTLM::Client::Session do
17
19
  session.authenticate!
18
20
  end
19
21
 
20
- it 'checks if it is anonymous' do
21
- expect(session).to receive(:is_anonymous?).at_least(1).times.and_call_original
22
- session.authenticate!
23
- end
24
-
25
22
  it 'returns a Type3 message' do
26
23
  expect(session.authenticate!).to be_a Net::NTLM::Message::Type3
27
24
  expect(session.authenticate!).to be_a Net::NTLM::Message
28
25
  end
29
26
 
30
27
  context 'when it is anonymous' do
31
- before(:each) { allow(session).to receive(:is_anonymous?).and_return(true) }
32
- after(:each) { session.authenticate! }
28
+ let(:username) { '' }
29
+ let(:password) { '' }
33
30
 
34
31
  it 'uses the correct lm response' do
35
- expect(session).to_not receive(:lmv2_resp)
36
- expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params|
37
- expect(params).to include :lm_response
38
- expect(params[:lm_response]).to eq "\x00".b
39
- method.call(params)
40
- end
32
+ expect(session.authenticate!.lm_response).to eq "\x00".b
41
33
  end
42
34
 
43
35
  it 'uses the correct ntlm response' do
44
- expect(session).to_not receive(:ntlmv2_resp)
45
- expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params|
46
- expect(params).to include :ntlm_response
47
- expect(params[:ntlm_response]).to eq ''
48
- method.call(params)
49
- end
36
+ expect(session.authenticate!.ntlm_response).to eq ''
50
37
  end
51
38
  end
52
39
 
53
40
  context 'when it is not anonymous' do
54
- before(:each) { allow(session).to receive(:is_anonymous?).and_return(false) }
55
- after(:each) { session.authenticate! }
56
-
57
41
  it 'uses the correct lm response' do
58
- expect(session).to receive(:lmv2_resp).and_call_original
59
- expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params|
60
- expect(params).to include :lm_response
61
- expect(params[:lm_response].length).to be > 16
62
- method.call(params)
63
- end
42
+ expect(session.authenticate!.lm_response.length).to be > 16
64
43
  end
65
44
 
66
45
  it 'uses the correct ntlm response' do
67
- expect(session).to receive(:ntlmv2_resp).and_call_original
68
- expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params|
69
- expect(params).to include :ntlm_response
70
- expect(params[:ntlm_response].length).to be > 16
71
- method.call(params)
72
- end
46
+ expect(session.authenticate!.ntlm_response.length).to be > 16
73
47
  end
74
48
  end
75
49
  end
76
50
 
77
51
  describe '#calculate_user_session_key!' do
78
- it 'returns an all zero key when it is anonymous' do
79
- expect(session).to receive(:is_anonymous?).and_return(true)
80
- expect(session.send(:calculate_user_session_key!)).to eq "\x00".b * 16
52
+ context 'when it is anonymous' do
53
+ let(:username) { '' }
54
+ let(:password) { '' }
55
+
56
+ it 'returns an all zero key' do
57
+ expect(session.send(:calculate_user_session_key!)).to eq "\x00".b * 16
58
+ end
81
59
  end
82
60
 
83
61
  it 'returns a session key' do
84
- expect(session).to receive(:is_anonymous?).and_return(false)
85
- expect(session.send(:calculate_user_session_key!)).to_not eq "\x00".b * 16
62
+ session_key = session.send(:calculate_user_session_key!)
63
+ expect(session_key.bytesize).to eq 16
64
+ expect(session_key).to_not eq "\x00".b * 16
86
65
  end
87
66
  end
88
67