class RubySMB::SMB1::Tree

An SMB1 connected remote Tree, as returned by a

RubySMB::SMB1::Packet::TreeConnectRequest

Attributes

client[RW]

The client this Tree is connected through @!attribute [rw] client

@return [RubySMB::Client]
guest_permissions[RW]

The current Guest Share Permissions @!attribute [rw] guest_permissions

@return [RubySMB::SMB1::BitField::DirectoryAccessMask]
id[RW]

The Tree ID for this Tree @!attribute [rw] id

@return [Integer]
permissions[RW]

The current Maximal Share Permissions @!attribute [rw] permissions

@return [RubySMB::SMB1::BitField::DirectoryAccessMask]
share[RW]

The share path associated with this Tree @!attribute [rw] share

@return [String]

Public Class Methods

new(client:, share:, response:) click to toggle source
# File lib/ruby_smb/smb1/tree.rb, line 31
def initialize(client:, share:, response:)
  @client             = client
  @share              = share
  @id                 = response.smb_header.tid
  @guest_permissions  = response.parameter_block.guest_access_rights
  @permissions        = response.parameter_block.access_rights
end

Public Instance Methods

disconnect!() click to toggle source

Disconnects this Tree from the current session

@return [WindowsError::ErrorCode] the NTStatus sent back by the server. @raise [RubySMB::Error::InvalidPacket] if the response is not a TreeDisconnectResponse packet

# File lib/ruby_smb/smb1/tree.rb, line 43
def disconnect!
  request = RubySMB::SMB1::Packet::TreeDisconnectRequest.new
  request = set_header_fields(request)
  raw_response = client.send_recv(request)
  response = RubySMB::SMB1::Packet::TreeDisconnectResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::TreeDisconnectResponse::COMMAND,
      packet:         response
    )
  end
  response.status_code
end
list(directory: '\\', pattern: '*', unicode: true, type: RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindFileFullDirectoryInfo) click to toggle source

List `directory` on the remote share.

@example

tree = client.tree_connect("\\\\192.168.99.134\\Share")
tree.list(directory: "path\\to\\directory")

@param directory [String] path to the directory to be listed @param pattern [String] search pattern @param type [Class] file information class @return [Array] array of directory structures @raise [RubySMB::Error::InvalidPacket] if the response is not a Trans2 packet @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb1/tree.rb, line 174
def list(directory: '\\', pattern: '*', unicode: true,
         type: RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindFileFullDirectoryInfo)
  find_first_request = RubySMB::SMB1::Packet::Trans2::FindFirst2Request.new
  find_first_request = set_header_fields(find_first_request)
  find_first_request.smb_header.flags2.unicode  = 1 if unicode

  search_path = directory.dup
  search_path << '\\' unless search_path.end_with?('\\')
  search_path << pattern
  search_path = '\\' + search_path unless search_path.start_with?('\\')

  # Set the search parameters
  t2_params = find_first_request.data_block.trans2_parameters
  t2_params.search_attributes.hidden    = 1
  t2_params.search_attributes.system    = 1
  t2_params.search_attributes.directory = 1
  t2_params.flags.close_eos             = 1
  t2_params.flags.resume_keys           = 0
  t2_params.information_level           = type::CLASS_LEVEL
  t2_params.filename                    = search_path
  t2_params.search_count                = 10

  find_first_request = set_find_params(find_first_request)

  raw_response  = client.send_recv(find_first_request)
  response      = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::Trans2::FindFirst2Response::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end

  results = response.results(type, unicode: unicode)

  eos   = response.data_block.trans2_parameters.eos
  sid   = response.data_block.trans2_parameters.sid
  last  = results.last.file_name

  while eos.zero?
    find_next_request = RubySMB::SMB1::Packet::Trans2::FindNext2Request.new
    find_next_request = set_header_fields(find_next_request)
    find_next_request.smb_header.flags2.unicode   = 1 if unicode

    t2_params                             = find_next_request.data_block.trans2_parameters
    t2_params.sid                         = sid
    t2_params.flags.close_eos             = 1
    t2_params.flags.resume_keys           = 0
    t2_params.information_level           = type::CLASS_LEVEL
    t2_params.filename                    = last
    t2_params.search_count                = 10

    find_next_request = set_find_params(find_next_request)

    raw_response  = client.send_recv(find_next_request)
    response      = RubySMB::SMB1::Packet::Trans2::FindNext2Response.read(raw_response)
    unless response.valid?
      raise RubySMB::Error::InvalidPacket.new(
        expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
        expected_cmd:   RubySMB::SMB1::Packet::Trans2::FindNext2Response::COMMAND,
        packet:         response
      )
    end
    unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
      raise RubySMB::Error::UnexpectedStatusCode, response.status_code
    end

    results += response.results(type, unicode: unicode)

    eos   = response.data_block.trans2_parameters.eos
    last  = results.last.file_name
  end

  results
end
open_file(filename:, flags: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN, impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false) click to toggle source

Open a file on the remote share.

@example

tree = client.tree_connect("\\\\192.168.99.134\\Share")
tree.open_file(filename: "myfile")

@param filename [String] name of the file to be opened @param flags [BinData::Struct, Hash] flags to setup the request (see {RubySMB::SMB1::Packet::NtCreateAndxRequest}) @param options [RubySMB::SMB1::BitField::CreateOptions, Hash] flags that defines how the file should be created @param disposition [Integer] 32-bit field that defines how an already-existing file or a new file needs to be handled (constants are defined in {RubySMB::Dispositions}) @param impersonation [Integer] 32-bit field that defines the impersonation level (constants are defined in {RubySMB::ImpersonationLevels}) @param read [TrueClass, FalseClass] request a read access @param write [TrueClass, FalseClass] request a write access @param delete [TrueClass, FalseClass] request a delete access @return [RubySMB::SMB1::File] handle to the created file @raise [RubySMB::Error::InvalidPacket] if the response command is not SMB_COM_NT_CREATE_ANDX @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb1/tree.rb, line 83
def open_file(filename:, flags: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN,
              impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false)
  nt_create_andx_request = RubySMB::SMB1::Packet::NtCreateAndxRequest.new
  nt_create_andx_request = set_header_fields(nt_create_andx_request)

  nt_create_andx_request.parameter_block.ext_file_attributes.normal = 1

  if flags
    nt_create_andx_request.parameter_block.flags = flags
  else
    nt_create_andx_request.parameter_block.flags.request_extended_response = 1
  end

  if options
    nt_create_andx_request.parameter_block.create_options = options
  else
    nt_create_andx_request.parameter_block.create_options.directory_file     = 0
    nt_create_andx_request.parameter_block.create_options.non_directory_file = 1
  end

  if read
    nt_create_andx_request.parameter_block.share_access.share_read     = 1
    nt_create_andx_request.parameter_block.desired_access.read_data    = 1
    nt_create_andx_request.parameter_block.desired_access.read_ea      = 1
    nt_create_andx_request.parameter_block.desired_access.read_attr    = 1
    nt_create_andx_request.parameter_block.desired_access.read_control = 1
  end

  if write
    nt_create_andx_request.parameter_block.share_access.share_write   = 1
    nt_create_andx_request.parameter_block.desired_access.write_data  = 1
    nt_create_andx_request.parameter_block.desired_access.append_data = 1
    nt_create_andx_request.parameter_block.desired_access.write_ea    = 1
    nt_create_andx_request.parameter_block.desired_access.write_attr  = 1
  end

  if delete
    nt_create_andx_request.parameter_block.share_access.share_delete    = 1
    nt_create_andx_request.parameter_block.desired_access.delete_access = 1
  end

  nt_create_andx_request.parameter_block.impersonation_level = impersonation
  nt_create_andx_request.parameter_block.create_disposition  = disposition

  unicode_enabled = nt_create_andx_request.smb_header.flags2.unicode == 1
  nt_create_andx_request.data_block.file_name = add_null_termination(str: filename, unicode: unicode_enabled)

  raw_response = @client.send_recv(nt_create_andx_request)
  response = RubySMB::SMB1::Packet::NtCreateAndxResponse.read(raw_response)
  unless response.valid?
    if response.is_a?(RubySMB::SMB1::Packet::EmptyPacket) &&
         response.smb_header.protocol == RubySMB::SMB1::SMB_PROTOCOL_ID &&
         response.smb_header.command == response.original_command
      raise RubySMB::Error::InvalidPacket.new(
        'The response seems to be an SMB1 NtCreateAndxResponse but an '\
        'error occurs while parsing it. It is probably missing the '\
        'required extended information.'
      )
    end
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::NtCreateAndxResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end

  case response.parameter_block.resource_type
  when RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE, RubySMB::SMB1::ResourceType::MESSAGE_MODE_PIPE
    RubySMB::SMB1::Pipe.new(name: filename, tree: self, response: response)
  when RubySMB::SMB1::ResourceType::DISK
    RubySMB::SMB1::File.new(name: filename, tree: self, response: response)
  else
    raise RubySMB::Error::RubySMBError
  end
end
open_pipe(opts) click to toggle source
# File lib/ruby_smb/smb1/tree.rb, line 58
def open_pipe(opts)
  # Make sure we don't modify the caller's hash options
  opts = opts.dup
  opts[:filename] = opts[:filename].dup
  opts[:filename].prepend('\\') unless opts[:filename].start_with?('\\')
  open_file(**opts)
end
set_header_fields(request) click to toggle source

Sets a few preset header fields that will always be set the same way for Tree operations. This is, the TreeID and Extended Attributes.

@param [RubySMB::SMB::Packet] the request packet to modify @return [RubySMB::SMB::Packet] the modified packet.

# File lib/ruby_smb/smb1/tree.rb, line 259
def set_header_fields(request)
  request.smb_header.tid        = @id
  request.smb_header.flags2.eas = 1
  request
end

Private Instance Methods

add_null_termination(str:, unicode: false) click to toggle source

Add null termination to `str` in case it is not already null-terminated.

@str [String] the string to be null-terminated @unicode [TrueClass, FalseClass] True if the null-termination should be Unicode encoded @return [String] the null-terminated string

# File lib/ruby_smb/smb1/tree.rb, line 285
def add_null_termination(str:, unicode: false)
  null_termination = unicode ? "\x00".encode('UTF-16LE') : "\x00"
  if str.end_with?(null_termination)
    return str
  else
    return str + null_termination
  end
end
set_find_params(request) click to toggle source

Sets ParameterBlock options for FIND_FIRST2 and FIND_NEXT2 requests. In particular we need to do this to tell the server to ignore the Trans2DataBlock as we are not sending any GEA lists in this instance.

# File lib/ruby_smb/smb1/tree.rb, line 271
def set_find_params(request)
  request.parameter_block.data_count             = 0
  request.parameter_block.data_offset            = 0
  request.parameter_block.total_parameter_count  = request.parameter_block.parameter_count
  request.parameter_block.max_parameter_count    = request.parameter_block.parameter_count
  request.parameter_block.max_data_count         = 16_384
  request
end