ruby_smb 3.3.17 → 3.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ruby_smb/client/authentication.rb +53 -0
  3. data/lib/ruby_smb/client/negotiation.rb +10 -2
  4. data/lib/ruby_smb/client/tree_connect.rb +8 -1
  5. data/lib/ruby_smb/client.rb +16 -5
  6. data/lib/ruby_smb/rap/net_share_enum.rb +166 -0
  7. data/lib/ruby_smb/rap.rb +10 -0
  8. data/lib/ruby_smb/smb1/commands.rb +1 -0
  9. data/lib/ruby_smb/smb1/packet/negotiate_response.rb +11 -0
  10. data/lib/ruby_smb/smb1/packet/nt_create_andx_response.rb +19 -6
  11. data/lib/ruby_smb/smb1/packet/open_andx_request.rb +39 -0
  12. data/lib/ruby_smb/smb1/packet/open_andx_response.rb +40 -0
  13. data/lib/ruby_smb/smb1/packet/session_setup_legacy_request.rb +2 -2
  14. data/lib/ruby_smb/smb1/packet/session_setup_legacy_response.rb +11 -0
  15. data/lib/ruby_smb/smb1/packet/trans2/find_first2_response.rb +53 -13
  16. data/lib/ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard.rb +39 -0
  17. data/lib/ruby_smb/smb1/packet/trans2/find_information_level.rb +1 -0
  18. data/lib/ruby_smb/smb1/packet/trans2/win9x_framing.rb +68 -0
  19. data/lib/ruby_smb/smb1/packet/trans2.rb +1 -0
  20. data/lib/ruby_smb/smb1/packet/tree_connect_request.rb +1 -1
  21. data/lib/ruby_smb/smb1/packet/tree_connect_response.rb +10 -1
  22. data/lib/ruby_smb/smb1/packet.rb +2 -0
  23. data/lib/ruby_smb/smb1/pipe.rb +2 -0
  24. data/lib/ruby_smb/smb1/tree.rb +113 -9
  25. data/lib/ruby_smb/version.rb +1 -1
  26. data/lib/ruby_smb.rb +1 -0
  27. data/spec/lib/ruby_smb/client_spec.rb +2 -1
  28. data/spec/lib/ruby_smb/rap/net_share_enum_spec.rb +185 -0
  29. data/spec/lib/ruby_smb/smb1/packet/trans2/win9x_framing_spec.rb +113 -0
  30. data/spec/lib/ruby_smb/smb1/tree_spec.rb +188 -2
  31. metadata +12 -2
@@ -0,0 +1,39 @@
1
+ module RubySMB
2
+ module SMB1
3
+ module Packet
4
+ module Trans2
5
+ module FindInformationLevel
6
+ # SMB_INFO_STANDARD find result entry, as defined in
7
+ # [MS-CIFS 2.2.8.1.1 SMB_INFO_STANDARD](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/b7cc0966-f87d-41a6-aa1a-48526a9cc729).
8
+ # Used by TRANS2_FIND_FIRST2/FIND_NEXT2 on legacy servers
9
+ # (e.g. Windows 95/98/ME) that don't support NT LANMAN info levels.
10
+ #
11
+ # Unlike NT info levels, these entries have no next_offset field;
12
+ # they are packed sequentially with a variable-length filename.
13
+ # The optional leading ResumeKey (4 bytes) is only present when the
14
+ # SMB_FIND_RETURN_RESUME_KEYS flag is set in the request; this
15
+ # implementation does not set that flag and so omits the field.
16
+ class FindInfoStandard < BinData::Record
17
+ CLASS_LEVEL = FindInformationLevel::SMB_INFO_STANDARD
18
+
19
+ endian :little
20
+
21
+ uint16 :creation_date, label: 'Creation Date (SMB_DATE)'
22
+ uint16 :creation_time, label: 'Creation Time (SMB_TIME)'
23
+ uint16 :last_access_date, label: 'Last Access Date (SMB_DATE)'
24
+ uint16 :last_access_time, label: 'Last Access Time (SMB_TIME)'
25
+ uint16 :last_write_date, label: 'Last Write Date (SMB_DATE)'
26
+ uint16 :last_write_time, label: 'Last Write Time (SMB_TIME)'
27
+ uint32 :data_size, label: 'File Data Size'
28
+ uint32 :allocation_size, label: 'Allocation Size'
29
+ uint16 :file_attributes, label: 'File Attributes'
30
+ uint8 :file_name_length, label: 'File Name Length',
31
+ initial_value: -> { file_name.to_s.bytesize }
32
+ string :file_name, label: 'File Name',
33
+ read_length: -> { file_name_length }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -38,6 +38,7 @@ module RubySMB
38
38
 
39
39
  require 'ruby_smb/smb1/packet/trans2/find_information_level/find_file_both_directory_info'
40
40
  require 'ruby_smb/smb1/packet/trans2/find_information_level/find_file_full_directory_info'
41
+ require 'ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard'
41
42
  end
42
43
  end
43
44
  end
@@ -0,0 +1,68 @@
1
+ module RubySMB
2
+ module SMB1
3
+ module Packet
4
+ module Trans2
5
+ # Shared workaround for pre-NT / LAN Manager-era servers (observed on
6
+ # Windows 9x / ME) that pack `trans2_parameters` directly after
7
+ # `byte_count` with no 4-byte-alignment pad, and `trans2_data` with
8
+ # whatever padding they feel like — always smaller than the NT-style
9
+ # alignment BinData unconditionally assumes via {DataBlock#pad1_length}
10
+ # and {DataBlock#pad2_length}. When that happens both sections land in
11
+ # the wrong place and `eos`, `sid`, `last_name_offset`, and every
12
+ # entry in the data buffer come back garbled.
13
+ #
14
+ # Fixing this in BinData itself (by having pad1/pad2 consult
15
+ # `parameter_block.parameter_offset` / `data_offset`) is the natural
16
+ # design, but cross-field lookups during field-read callbacks corrupt
17
+ # BinData's registered-class resolution cache, causing unrelated
18
+ # Trans2 responses to round-trip their `parameter_block` / `data_block`
19
+ # through the base classes instead of the concrete subclasses. So
20
+ # instead we surface the raw response bytes at the call site and let
21
+ # the response slice both sections from the offsets the server
22
+ # reported in its `parameter_block`.
23
+ #
24
+ # Mix into any {RubySMB::SMB1::Packet::Trans2} response whose caller
25
+ # holds on to the raw response bytes. The response itself must have
26
+ # the standard {Trans2::Response::ParameterBlock} shape
27
+ # (`parameter_offset` / `parameter_count` / `data_offset` /
28
+ # `data_count`) and a `data_block` with `trans2_parameters` and
29
+ # `trans2_data.buffer` fields — every concrete Trans2 response does.
30
+ #
31
+ # Same slicing pattern as {RubySMB::Rap::NetShareEnum#parse_net_share_enum_response}
32
+ # uses for the sibling Trans (not Trans2) response type.
33
+ module Win9xFraming
34
+ # Returns `[effective_trans2_parameters, effective_trans2_data_bytes]`
35
+ # when the server's layout differs from BinData's, or `[nil, nil]`
36
+ # when BinData already read the full buffer (standard NT-era servers).
37
+ #
38
+ # When a non-nil pair is returned, callers should prefer the override
39
+ # values over the BinData-parsed ones:
40
+ #
41
+ # params_ovr, data_ovr = response.win9x_trans2_overrides(raw)
42
+ # params = params_ovr || response.data_block.trans2_parameters
43
+ # data = data_ovr || response.data_block.trans2_data.buffer.to_binary_s
44
+ #
45
+ # @param raw_response [String] the raw bytes the response was read from.
46
+ # @return [Array(BinData::Record, String), Array(nil, nil)]
47
+ def win9x_trans2_overrides(raw_response)
48
+ declared_data = parameter_block.data_count.to_i
49
+ parsed_data = data_block.trans2_data.buffer.to_binary_s.bytesize
50
+ return [nil, nil] if declared_data.zero? || parsed_data == declared_data
51
+
52
+ param_offset = parameter_block.parameter_offset.to_i
53
+ param_count = parameter_block.parameter_count.to_i
54
+ data_offset = parameter_block.data_offset.to_i
55
+ return [nil, nil] if raw_response.bytesize < data_offset + declared_data
56
+ return [nil, nil] if raw_response.bytesize < param_offset + param_count
57
+
58
+ params_bytes = raw_response.byteslice(param_offset, param_count)
59
+ params_class = data_block.trans2_parameters.class
60
+ params = params_class.read(params_bytes)
61
+ data_bytes = raw_response.byteslice(data_offset, declared_data)
62
+ [params, data_bytes]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -8,6 +8,7 @@ module RubySMB
8
8
  require 'ruby_smb/smb1/packet/trans2/query_information_level'
9
9
  require 'ruby_smb/smb1/packet/trans2/query_fs_information_level'
10
10
  require 'ruby_smb/smb1/packet/trans2/data_block'
11
+ require 'ruby_smb/smb1/packet/trans2/win9x_framing'
11
12
  require 'ruby_smb/smb1/packet/trans2/subcommands'
12
13
  require 'ruby_smb/smb1/packet/trans2/request'
13
14
  require 'ruby_smb/smb1/packet/trans2/request_secondary'
@@ -15,7 +15,7 @@ module RubySMB
15
15
 
16
16
  # The {RubySMB::SMB1::DataBlock} specific to this packet type.
17
17
  class DataBlock < RubySMB::SMB1::DataBlock
18
- stringz :password, label: 'Password Field', initial_value: '', length: -> { parent.parameter_block.password_length }
18
+ string :password, label: 'Password Field', initial_value: "\x00", length: -> { parent.parameter_block.password_length }
19
19
  choice :path, selection: -> { parent.smb_header.flags2.unicode } do
20
20
  stringz 0
21
21
  stringz16 1
@@ -9,15 +9,24 @@ module RubySMB
9
9
  # A SMB1 Parameter Block as defined by the {SessionSetupResponse}
10
10
  class ParameterBlock < RubySMB::SMB1::ParameterBlock
11
11
  and_x_block :andx_block
12
- optional_support :optional_support
12
+ optional_support :optional_support, onlyif: -> { word_count >= 3 }
13
13
  directory_access_mask :access_rights, label: 'Maximal Share Access Rights', onlyif: -> { word_count >= 5 }
14
14
  directory_access_mask :guest_access_rights, label: 'Guest Share Access Rights', onlyif: -> { word_count == 7 }
15
15
  end
16
16
 
17
17
  # Represents the specific layout of the DataBlock for a {SessionSetupResponse} Packet.
18
+ # Windows 95/98/ME may return a minimal DataBlock without the native file system.
18
19
  class DataBlock < RubySMB::SMB1::DataBlock
19
20
  stringz :service, label: 'Service Type'
20
21
  stringz :native_file_system, label: 'Native File System'
22
+
23
+ # Override to handle Win95 responses that may omit native_file_system.
24
+ def do_read(io)
25
+ byte_count.do_read(io)
26
+ return unless byte_count > 0
27
+ service.do_read(io)
28
+ native_file_system.do_read(io) if byte_count > service.num_bytes
29
+ end
21
30
  end
22
31
 
23
32
  smb_header :smb_header
@@ -22,6 +22,8 @@ module RubySMB
22
22
  require 'ruby_smb/smb1/packet/trans'
23
23
  require 'ruby_smb/smb1/packet/trans2'
24
24
  require 'ruby_smb/smb1/packet/nt_trans'
25
+ require 'ruby_smb/smb1/packet/open_andx_request'
26
+ require 'ruby_smb/smb1/packet/open_andx_response'
25
27
  require 'ruby_smb/smb1/packet/nt_create_andx_request'
26
28
  require 'ruby_smb/smb1/packet/nt_create_andx_response'
27
29
  require 'ruby_smb/smb1/packet/read_andx_request'
@@ -36,6 +36,8 @@ module RubySMB
36
36
  extend RubySMB::Dcerpc::Icpr
37
37
  when 'efsrpc', '\\efsrpc'
38
38
  extend RubySMB::Dcerpc::Efsrpc
39
+ when 'lanman', 'LANMAN', '\\PIPE\\LANMAN', '\\lanman'
40
+ extend RubySMB::Rap::NetShareEnum
39
41
  end
40
42
  super(tree: tree, response: response, name: name)
41
43
  end
@@ -3,6 +3,11 @@ module RubySMB
3
3
  # An SMB1 connected remote Tree, as returned by a
4
4
  # [RubySMB::SMB1::Packet::TreeConnectRequest]
5
5
  class Tree
6
+ # Exposes #net_share_enum directly on the tree for callers that need
7
+ # RAP against \PIPE\LANMAN without opening the pipe (Win9x servers do
8
+ # not permit OPEN_ANDX on it).
9
+ include RubySMB::Rap::NetShareEnum
10
+
6
11
  # The client this Tree is connected through
7
12
  # @!attribute [rw] client
8
13
  # @return [RubySMB::Client]
@@ -102,9 +107,11 @@ module RubySMB
102
107
  # @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
103
108
  def list(directory: '\\', pattern: '*', unicode: true,
104
109
  type: RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindFileFullDirectoryInfo)
110
+ info_standard = (type == RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindInfoStandard)
111
+
105
112
  find_first_request = RubySMB::SMB1::Packet::Trans2::FindFirst2Request.new
106
113
  find_first_request = set_header_fields(find_first_request)
107
- find_first_request.smb_header.flags2.unicode = 1 if unicode
114
+ find_first_request.smb_header.flags2.unicode = 1 if unicode && !info_standard
108
115
 
109
116
  search_path = directory.dup
110
117
  search_path << '\\' unless search_path.end_with?('\\')
@@ -120,7 +127,7 @@ module RubySMB
120
127
  t2_params.flags.resume_keys = 0
121
128
  t2_params.information_level = type::CLASS_LEVEL
122
129
  t2_params.filename = search_path
123
- t2_params.search_count = 10
130
+ t2_params.search_count = info_standard ? 255 : 10
124
131
 
125
132
  find_first_request = set_find_params(find_first_request)
126
133
 
@@ -137,13 +144,19 @@ module RubySMB
137
144
  raise RubySMB::Error::UnexpectedStatusCode, response.status_code
138
145
  end
139
146
 
140
- results = response.results(type, unicode: unicode)
147
+ t2p_override, t2d_override = response.win9x_trans2_overrides(raw_response)
148
+ results = if t2d_override
149
+ response.results(type, unicode: unicode, buffer: t2d_override)
150
+ else
151
+ response.results(type, unicode: unicode)
152
+ end
141
153
 
142
- eos = response.data_block.trans2_parameters.eos
143
- sid = response.data_block.trans2_parameters.sid
144
- last = results.last.file_name
154
+ effective_params = t2p_override || response.data_block.trans2_parameters
155
+ eos = effective_params.eos
156
+ sid = effective_params.sid
157
+ last = results.last&.file_name
145
158
 
146
- while eos.zero?
159
+ while eos.zero? && last
147
160
  find_next_request = RubySMB::SMB1::Packet::Trans2::FindNext2Request.new
148
161
  find_next_request = set_header_fields(find_next_request)
149
162
  find_next_request.smb_header.flags2.unicode = 1 if unicode
@@ -171,8 +184,10 @@ module RubySMB
171
184
  raise RubySMB::Error::UnexpectedStatusCode, response.status_code
172
185
  end
173
186
 
174
- results += response.results(type, unicode: unicode)
187
+ batch = response.results(type, unicode: unicode)
188
+ break if batch.empty?
175
189
 
190
+ results += batch
176
191
  eos = response.data_block.trans2_parameters.eos
177
192
  last = results.last.file_name
178
193
  end
@@ -195,6 +210,9 @@ module RubySMB
195
210
 
196
211
  def _open(filename:, flags: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN,
197
212
  impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false)
213
+ unless client.server_supports_nt_smbs
214
+ return _open_andx(filename: filename, disposition: disposition, read: read, write: write)
215
+ end
198
216
  nt_create_andx_request = RubySMB::SMB1::Packet::NtCreateAndxRequest.new
199
217
  nt_create_andx_request = set_header_fields(nt_create_andx_request)
200
218
 
@@ -272,6 +290,91 @@ module RubySMB
272
290
  end
273
291
  end
274
292
 
293
+ # Open a file or pipe using SMB_COM_OPEN_ANDX (0x2D), the LAN Manager 1.0
294
+ # open command used by Windows 95/98/ME and other servers that don't
295
+ # advertise the NT SMBs capability. Accepts the same NT-style disposition
296
+ # constants as {#_open} and maps them to the OpenMode encoding defined in
297
+ # MS-CIFS 2.2.4.41.1.
298
+ #
299
+ # @param filename [String] path to the file on the share
300
+ # @param disposition [Integer] a RubySMB::Dispositions constant
301
+ # @param read [Boolean] request read access
302
+ # @param write [Boolean] request write access
303
+ # @return [RubySMB::SMB1::File, RubySMB::SMB1::Pipe] the opened resource
304
+ # @raise [RubySMB::Error::InvalidPacket] if the response is not valid
305
+ # @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
306
+ def _open_andx(filename:, disposition:, read: true, write: false)
307
+ request = RubySMB::SMB1::Packet::OpenAndxRequest.new
308
+ request = set_header_fields(request)
309
+ request.smb_header.flags2.unicode = 0
310
+
311
+ access = 0x0040 # sharing: deny-nothing
312
+ if read && write
313
+ access |= 0x02
314
+ elsif write
315
+ access |= 0x01
316
+ end
317
+
318
+ request.parameter_block.access_mode = access
319
+ # search_attributes / file_attributes are SMB_FILE_ATTRIBUTES BitField
320
+ # records, not plain uint16s — assign through #read to avoid BinData's
321
+ # each_pair-on-Integer NoMethodError when given a literal mask.
322
+ request.parameter_block.search_attributes.read([0x0016].pack('v'))
323
+ request.parameter_block.file_attributes.read([(write ? 0x0020 : 0x0000)].pack('v'))
324
+ request.parameter_block.open_mode = nt_disposition_to_open_mode(disposition)
325
+
326
+ fname = filename.dup
327
+ fname.prepend('\\') unless fname.start_with?('\\')
328
+ request.data_block.file_name = fname
329
+
330
+ raw_response = client.send_recv(request)
331
+ response = RubySMB::SMB1::Packet::OpenAndxResponse.read(raw_response)
332
+ unless response.valid?
333
+ raise RubySMB::Error::InvalidPacket.new(
334
+ expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
335
+ expected_cmd: RubySMB::SMB1::Packet::OpenAndxResponse::COMMAND,
336
+ packet: response
337
+ )
338
+ end
339
+ unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
340
+ raise RubySMB::Error::UnexpectedStatusCode, response.status_code
341
+ end
342
+
343
+ build_open_andx_handle(filename, response)
344
+ end
345
+
346
+ # Map an NT-style disposition (RubySMB::Dispositions) to an
347
+ # SMB_COM_OPEN_ANDX OpenMode word. FileExistsOpts is bits 0-1
348
+ # (0=fail, 1=open, 2=truncate); CreateFile is bit 4.
349
+ def nt_disposition_to_open_mode(disposition)
350
+ case disposition
351
+ when RubySMB::Dispositions::FILE_OPEN then 0x0001
352
+ when RubySMB::Dispositions::FILE_CREATE then 0x0010
353
+ when RubySMB::Dispositions::FILE_OPEN_IF then 0x0011
354
+ when RubySMB::Dispositions::FILE_OVERWRITE then 0x0002
355
+ when RubySMB::Dispositions::FILE_OVERWRITE_IF,
356
+ RubySMB::Dispositions::FILE_SUPERSEDE then 0x0012
357
+ else
358
+ raise RubySMB::Error::RubySMBError,
359
+ "Unsupported disposition for SMB_COM_OPEN_ANDX: #{disposition}"
360
+ end
361
+ end
362
+
363
+ def build_open_andx_handle(filename, response)
364
+ unless response.parameter_block.resource_type == RubySMB::SMB1::ResourceType::DISK
365
+ raise RubySMB::Error::RubySMBError,
366
+ "SMB_COM_OPEN_ANDX resource type 0x#{response.parameter_block.resource_type.to_s(16)} not supported"
367
+ end
368
+ file = RubySMB::SMB1::File.allocate
369
+ file.tree = self
370
+ file.name = filename
371
+ file.fid = response.parameter_block.fid
372
+ file.size = response.parameter_block.file_data_size
373
+ file.size_on_disk = response.parameter_block.file_data_size
374
+ file.attributes = response.parameter_block.file_attributes
375
+ file
376
+ end
377
+
275
378
  # Sets ParameterBlock options for FIND_FIRST2 and
276
379
  # FIND_NEXT2 requests. In particular we need to do this
277
380
  # to tell the server to ignore the Trans2DataBlock as we are
@@ -281,7 +384,8 @@ module RubySMB
281
384
  request.parameter_block.data_offset = 0
282
385
  request.parameter_block.total_parameter_count = request.parameter_block.parameter_count
283
386
  request.parameter_block.max_parameter_count = request.parameter_block.parameter_count
284
- request.parameter_block.max_data_count = 16_384
387
+ max_data = [16_384, client.server_max_buffer_size].min
388
+ request.parameter_block.max_data_count = max_data
285
389
  request
286
390
  end
287
391
 
@@ -1,3 +1,3 @@
1
1
  module RubySMB
2
- VERSION = '3.3.17'.freeze
2
+ VERSION = '3.3.19'.freeze
3
3
  end
data/lib/ruby_smb.rb CHANGED
@@ -23,6 +23,7 @@ module RubySMB
23
23
  require 'ruby_smb/dispatcher'
24
24
  require 'ruby_smb/version'
25
25
  require 'ruby_smb/smb2'
26
+ require 'ruby_smb/rap'
26
27
  require 'ruby_smb/smb1'
27
28
  require 'ruby_smb/client'
28
29
  require 'ruby_smb/crypto'
@@ -754,7 +754,7 @@ RSpec.describe RubySMB::Client do
754
754
  it 'sets the expected fields of the SessionRequest packet' do
755
755
  name = 'NBNAMESPEC'
756
756
  called_name = 'NBNAMESPEC '
757
- calling_name = " \x00"
757
+ calling_name = "WORKSTATION \x00"
758
758
 
759
759
  session_packet = client.session_request_packet(name)
760
760
  expect(session_packet).to be_a(RubySMB::Nbss::SessionRequest)
@@ -2438,6 +2438,7 @@ RSpec.describe RubySMB::Client do
2438
2438
  end
2439
2439
  end
2440
2440
  end
2441
+
2441
2442
  end
2442
2443
  end
2443
2444
 
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Rap::NetShareEnum do
4
+ let(:client) { instance_double(RubySMB::Client) }
5
+ let(:tree) { instance_double(RubySMB::SMB1::Tree, id: 1, client: client) }
6
+ let(:pipe) do
7
+ captured_tree = tree
8
+ p = RubySMB::SMB1::Pipe.allocate
9
+ p.instance_variable_set(:@tree, captured_tree)
10
+ p.define_singleton_method(:tree) { captured_tree }
11
+ p.extend(described_class)
12
+ p
13
+ end
14
+
15
+ def build_rap_response(status:, entries: [])
16
+ resp = RubySMB::SMB1::Packet::Trans::Response.new
17
+ params = RubySMB::Rap::NetShareEnum::Response.new(
18
+ status: status, converter: 0, entry_count: entries.length, available: entries.length
19
+ )
20
+ data = entries.map do |e|
21
+ si = RubySMB::Rap::NetShareEnum::ShareInfo1.new(
22
+ netname: e[:name], pad1: 0, share_type: e[:type], remark_offset: 0
23
+ )
24
+ si.to_binary_s
25
+ end.join
26
+ resp.data_block.trans_parameters = params.to_binary_s
27
+ resp.data_block.trans_data = data
28
+ resp.to_binary_s
29
+ end
30
+
31
+ describe '.new request layout' do
32
+ it 'encodes the RAP NetShareEnum opcode, descriptors, level and buffer size' do
33
+ bytes = RubySMB::Rap::NetShareEnum::Request.new.to_binary_s
34
+ expect(bytes[0, 2]).to eq([0].pack('v')) # opcode
35
+ expect(bytes[2, 6]).to eq("WrLeh\x00") # param descriptor
36
+ expect(bytes[8, 7]).to eq("B13BWz\x00") # data descriptor
37
+ expect(bytes[15, 2]).to eq([1].pack('v')) # info level 1
38
+ expect(bytes[17, 2]).to eq([0x1000].pack('v')) # receive buffer size
39
+ end
40
+ end
41
+
42
+ describe '#net_share_enum' do
43
+ it 'returns the parsed share list on RAP status 0' do
44
+ entries = [
45
+ { name: 'IPC$', type: 0x0003 }, # STYPE_IPC
46
+ { name: 'DATA', type: 0x0000 } # STYPE_DISKTREE
47
+ ]
48
+ allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
49
+ expect(pipe.net_share_enum).to eq([
50
+ { name: 'IPC$', type: 'IPC' },
51
+ { name: 'DATA', type: 'DISK' }
52
+ ])
53
+ end
54
+
55
+ it 'stringifies all MS-RAP 2.5.14 base share types' do
56
+ entries = [
57
+ { name: 'DSK', type: 0x0000 },
58
+ { name: 'PRN', type: 0x0001 },
59
+ { name: 'DEV', type: 0x0002 },
60
+ { name: 'IPC$', type: 0x0003 }
61
+ ]
62
+ allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
63
+ expect(pipe.net_share_enum.map { |s| s[:type] }).to eq(%w[DISK PRINTER DEVICE IPC])
64
+ end
65
+
66
+ it 'appends SPECIAL / TEMPORARY modifiers to the base type string' do
67
+ entries = [
68
+ { name: 'HIDDEN$', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL },
69
+ { name: 'TMP', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY },
70
+ { name: 'IPCH$', type: 0x0003 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL |
71
+ RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY }
72
+ ]
73
+ allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
74
+ expect(pipe.net_share_enum.map { |s| s[:type] }).to eq([
75
+ 'DISK|SPECIAL',
76
+ 'DISK|TEMPORARY',
77
+ 'IPC|SPECIAL|TEMPORARY'
78
+ ])
79
+ end
80
+
81
+ it 'formats an unknown base type code as UNKNOWN(0xXXXX)' do
82
+ entries = [{ name: 'Q', type: 0x0007 }]
83
+ allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
84
+ expect(pipe.net_share_enum.first[:type]).to eq('UNKNOWN(0x0007)')
85
+ end
86
+
87
+ it 'sends a Trans request targeting \\PIPE\\LANMAN with the tree id' do
88
+ allow(client).to receive(:send_recv) do |request|
89
+ expect(request).to be_a(RubySMB::SMB1::Packet::Trans::Request)
90
+ expect(request.smb_header.tid).to eq(tree.id)
91
+ expect(request.smb_header.flags2.unicode).to eq(0)
92
+ expect(request.data_block.name.to_s).to eq("\\PIPE\\LANMAN".b)
93
+ expect(request.data_block.trans_parameters.to_s).to eq(RubySMB::Rap::NetShareEnum::Request.new.to_binary_s)
94
+ build_rap_response(status: 0)
95
+ end
96
+ pipe.net_share_enum
97
+ end
98
+
99
+ it 'raises RubySMBError when the RAP status is non-zero' do
100
+ allow(client).to receive(:send_recv).and_return(build_rap_response(status: 5))
101
+ expect {
102
+ pipe.net_share_enum
103
+ }.to raise_error(RubySMB::Error::RubySMBError, /RAP NetShareEnum failed with status 0x5/)
104
+ end
105
+
106
+ it 'raises InvalidPacket when the RAP params are truncated' do
107
+ resp = RubySMB::SMB1::Packet::Trans::Response.new
108
+ resp.data_block.trans_parameters = "\x00\x00\x00"
109
+ resp.data_block.trans_data = ''
110
+ allow(client).to receive(:send_recv).and_return(resp.to_binary_s)
111
+ expect {
112
+ pipe.net_share_enum
113
+ }.to raise_error(RubySMB::Error::InvalidPacket)
114
+ end
115
+
116
+ it 'raises UnexpectedStatusCode when the SMB status is not success' do
117
+ resp = RubySMB::SMB1::Packet::Trans::Response.new
118
+ resp.smb_header.nt_status = WindowsError::NTStatus::STATUS_ACCESS_DENIED.value
119
+ resp.data_block.trans_parameters = RubySMB::Rap::NetShareEnum::Response.new(
120
+ status: 0, converter: 0, entry_count: 0, available: 0
121
+ ).to_binary_s
122
+ resp.data_block.trans_data = ''
123
+ allow(client).to receive(:send_recv).and_return(resp.to_binary_s)
124
+ expect {
125
+ pipe.net_share_enum
126
+ }.to raise_error(RubySMB::Error::UnexpectedStatusCode)
127
+ end
128
+ end
129
+
130
+ describe 'Win9x-style response layout (no 4-byte pad before trans_parameters)' do
131
+ it 'slices trans_parameters using the server-reported parameter_offset' do
132
+ # Win9x packs parameters at offset 55 (immediately after byte_count)
133
+ # rather than padding to a 4-byte boundary. Build a response matching
134
+ # that layout by hand.
135
+ entry = RubySMB::Rap::NetShareEnum::ShareInfo1.new(
136
+ netname: 'ABCDEFGHIJKL', pad1: 0, share_type: 0, remark_offset: 0
137
+ ).to_binary_s
138
+ params = RubySMB::Rap::NetShareEnum::Response.new(
139
+ status: 0, converter: 0, entry_count: 1, available: 1
140
+ ).to_binary_s
141
+
142
+ # 32-byte SMB1 header (command=0x25 SMB_COM_TRANSACTION, status=0).
143
+ header = "\xffSMB\x25".b + ("\x00".b * 27)
144
+ parameter_offset = 32 + 1 + 20 + 2 # right after byte_count
145
+ data_offset = parameter_offset + params.bytesize
146
+
147
+ parameter_block = [
148
+ params.bytesize, # total_parameter_count
149
+ entry.bytesize, # total_data_count
150
+ 0, # reserved
151
+ params.bytesize, # parameter_count
152
+ parameter_offset, # parameter_offset (55 - no pad)
153
+ 0, # parameter_displacement
154
+ entry.bytesize, # data_count
155
+ data_offset, # data_offset
156
+ 0 # data_displacement
157
+ ].pack('v9') + "\x00\x00".b # setup_count + reserved2
158
+
159
+ byte_count = [params.bytesize + entry.bytesize].pack('v')
160
+ raw = header + "\x0a".b + parameter_block + byte_count + params + entry
161
+ # BinData's default Trans::Response#pad1_length forces a 4-byte align
162
+ # and reads trans_parameters starting 1 byte past what the server sent.
163
+ # Pad the tail so BinData's (mis-aligned) read doesn't hit EOF before
164
+ # our parser takes over with the server-reported offsets.
165
+ raw << "\x00".b * 8
166
+
167
+ allow(client).to receive(:send_recv).and_return(raw)
168
+ shares = pipe.net_share_enum
169
+ expect(shares.length).to eq(1)
170
+ expect(shares[0][:name]).to eq('ABCDEFGHIJKL')
171
+ expect(shares[0][:type]).to eq('DISK')
172
+ end
173
+ end
174
+
175
+ describe 'SMB1::Pipe integration' do
176
+ it 'extends the pipe with NetShareEnum when opened as \\PIPE\\LANMAN' do
177
+ # Use a minimal response to drive Pipe#initialize through File#initialize.
178
+ nt_resp = RubySMB::SMB1::Packet::NtCreateAndxResponse.new
179
+ nt_resp.parameter_block.fid = 0x1001
180
+ nt_resp.parameter_block.resource_type = RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE
181
+ p = RubySMB::SMB1::Pipe.new(tree: tree, response: nt_resp, name: '\\PIPE\\LANMAN')
182
+ expect(p).to respond_to(:net_share_enum)
183
+ end
184
+ end
185
+ end