rapid7/ruby_smb

View on GitHub
lib/ruby_smb/smb2/file.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module RubySMB
  module SMB2
    # Represents a file on the Remote server that we can perform
    # various I/O operations on.
    class File
      # The maximum number of byte we want to read or write
      # in a single packet.
      MAX_PACKET_SIZE = 32_768

      # The {FileAttributes} for the file
      # @!attribute [rw] attributes
      #   @return [RubySMB::Fscc::FileAttributes]
      attr_accessor :attributes

      # The {Smb2FileId} for the file
      # @!attribute [rw] guid
      #   @return [RubySMB::Field::Smb2FileId]
      attr_accessor :guid

      # The last access date/time for the file
      # @!attribute [rw] last_access
      #   @return [DateTime]
      attr_accessor :last_access

      # The last change date/time for the file
      # @!attribute [rw] last_change
      #   @return [DateTime]
      attr_accessor :last_change

      # The last write date/time for the file
      # @!attribute [rw] last_write
      #   @return [DateTime]
      attr_accessor :last_write

      # The name of the file
      # @!attribute [rw] name
      #   @return [String]
      attr_accessor :name

      # The actual size, in bytes, of the file
      # @!attribute [rw] size
      #   @return [Integer]
      attr_accessor :size

      # The size in bytes that the file occupies on disk
      # @!attribute [rw] size_on_disk
      #   @return [Integer]
      attr_accessor :size_on_disk

      # The {RubySMB::SMB2::Tree} that this file belong to
      # @!attribute [rw] tree
      #   @return [RubySMB::SMB2::Tree]
      attr_accessor :tree

      # Whether or not the share associated with this tree connect needs to be encrypted (SMB 3.x)
      # @!attribute [rw] tree_connect_encrypt_data
      #   @return [Boolean]
      attr_accessor :tree_connect_encrypt_data

      def initialize(tree:, response:, name:, encrypt: false)
        raise ArgumentError, 'No Tree Provided' if tree.nil?
        raise ArgumentError, 'No Response Provided' if response.nil?

        @tree = tree
        @name = name

        @attributes   = response.file_attributes
        @guid         = response.file_id
        @last_access  = response.last_access.to_datetime
        @last_change  = response.last_change.to_datetime
        @last_write   = response.last_write.to_datetime
        @size         = response.end_of_file
        @size_on_disk = response.allocation_size
        @tree_connect_encrypt_data = encrypt
      end

      # Appends the supplied data to the end of the file.
      #
      # @param data [String] the data to write to the file
      # @return [WindowsError::ErrorCode] the NTStatus code returned from the operation
      def append(data:'')
        write(data: data, offset: size)
      end

      # Closes the handle to the remote file.
      #
      # @return [WindowsError::ErrorCode] the NTStatus code returned by the operation
      # @raise [RubySMB::Error::InvalidPacket] if the response is not a CloseResponse packet
      # @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
      def close
        close_request = set_header_fields(RubySMB::SMB2::Packet::CloseRequest.new)
        raw_response  = tree.client.send_recv(close_request, encrypt: @tree_connect_encrypt_data)
        response = RubySMB::SMB2::Packet::CloseResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::CloseResponse::COMMAND,
            packet:         response
          )
        end
        unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
          raise RubySMB::Error::UnexpectedStatusCode, response.status_code
        end
        response.status_code
      end

      # Read from the file, a specific number of bytes
      # from a specific offset. If no parameters are given
      # it will read the entire file.
      #
      # @param bytes [Integer] the number of bytes to read
      # @param offset [Integer] the byte offset in the file to start reading from
      # @return [String] the data read from the file
      # @raise [RubySMB::Error::InvalidPacket] if the response is not a ReadResponse packet
      # @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
      def read(bytes: size, offset: 0)
        max_read = tree.client.server_max_read_size
        max_read = 65536 unless tree.client.server_supports_multi_credit
        atomic_read_size = [bytes, max_read].min
        credit_charge = 0
        if tree.client.server_supports_multi_credit
          credit_charge = (atomic_read_size - 1) / 65536 + 1
        end

        read_request = read_packet(read_length: atomic_read_size, offset: offset, credit_charge: credit_charge)
        raw_response = tree.client.send_recv(read_request, encrypt: @tree_connect_encrypt_data)
        response     = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
            packet:         response
          )
        end
        unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
          raise RubySMB::Error::UnexpectedStatusCode, response.status_code
        end

        data = response.buffer.to_binary_s

        remaining_bytes = bytes - atomic_read_size

        while remaining_bytes > 0
          offset += atomic_read_size
          atomic_read_size = remaining_bytes if remaining_bytes < max_read

          read_request = read_packet(read_length: atomic_read_size, offset: offset, credit_charge: credit_charge)
          raw_response = tree.client.send_recv(read_request, encrypt: @tree_connect_encrypt_data)
          response     = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
          unless response.valid?
            raise RubySMB::Error::InvalidPacket.new(
              expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
              expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
              packet:         response
            )
          end
          unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
            raise RubySMB::Error::UnexpectedStatusCode, response.status_code
          end

          data << response.buffer.to_binary_s
          remaining_bytes -= atomic_read_size
        end
        data
      end

      # Crafts the ReadRequest packet to be sent for read operations.
      #
      # @param bytes [Integer] the number of bytes to read
      # @param offset [Integer] the byte offset in the file to start reading from
      # @param credit_charge [Integer] the number of credits that this request consumes
      # @return [RubySMB::SMB2::Packet::ReadRequest] the data read from the file
      def read_packet(read_length: 0, offset: 0, credit_charge: 1)
        read_request = set_header_fields(RubySMB::SMB2::Packet::ReadRequest.new)
        read_request.read_length  = read_length
        read_request.offset       = offset
        read_request.smb2_header.credit_charge = credit_charge
        read_request
      end

      def send_recv_read(read_length: 0, offset: 0)
        read_request = read_packet(read_length: read_length, offset: offset)
        raw_response = tree.client.send_recv(read_request, encrypt: @tree_connect_encrypt_data)
        response = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
            packet:         response
          )
        end
        unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
          raise RubySMB::Error::UnexpectedStatusCode, response.status_code
        end
        response.buffer.to_binary_s
      end

      # Delete a file on close
      #
      # @return [WindowsError::ErrorCode] the NTStatus Response code
      # @raise [RubySMB::Error::InvalidPacket] if the response is not a SetInfoResponse packet
      def delete
        raw_response = tree.client.send_recv(delete_packet, encrypt: @tree_connect_encrypt_data)
        response = RubySMB::SMB2::Packet::SetInfoResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::SetInfoResponse::COMMAND,
            packet:         response
          )
        end
        response.smb2_header.nt_status.to_nt_status
      end

      # Crafts the SetInfoRequest packet to be sent for delete operations.
      #
      # @return [RubySMB::SMB2::Packet::SetInfoRequest] the set info packet
      def delete_packet
        delete_request                       = set_header_fields(RubySMB::SMB2::Packet::SetInfoRequest.new)
        delete_request.file_info_class       = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION
        delete_request.buffer.delete_pending = 1
        delete_request
      end

      # Sets the header fields that we have to set on every packet
      # we send for File operations.
      # @param request [RubySMB::GenericPacket] the request packet to set fields on
      # @return  [RubySMB::GenericPacket] the rmodified request packet
      def set_header_fields(request)
        request         = tree.set_header_fields(request)
        request.file_id = guid
        request
      end

      # Write the supplied data to the file at the given offset.
      #
      # @param data [String] the data to write to the file
      # @param offset [Integer] the offset in the file to start writing from
      # @return [WindowsError::ErrorCode] the NTStatus code returned from the operation
      # @raise [RubySMB::Error::InvalidPacket] if the response is not a WriteResponse packet
      def write(data:'', offset: 0)
        max_write = tree.client.server_max_write_size
        max_write = 65536 unless tree.client.server_supports_multi_credit
        buffer            = data.dup
        bytes             = data.length
        atomic_write_size = [bytes, max_write].min
        credit_charge = 0
        if tree.client.server_supports_multi_credit
          credit_charge = (atomic_write_size - 1) / 65536 + 1
        end

        while buffer.length > 0 do
          write_request = write_packet(data: buffer.slice!(0, atomic_write_size), offset: offset, credit_charge: credit_charge)
          raw_response  = tree.client.send_recv(write_request, encrypt: @tree_connect_encrypt_data)
          response      = RubySMB::SMB2::Packet::WriteResponse.read(raw_response)
          unless response.valid?
            raise RubySMB::Error::InvalidPacket.new(
              expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
              expected_cmd:   RubySMB::SMB2::Packet::WriteResponse::COMMAND,
              packet:         response
            )
          end
          status        = response.smb2_header.nt_status.to_nt_status

          offset += atomic_write_size
          return status unless status == WindowsError::NTStatus::STATUS_SUCCESS
        end

        status
      end

      # Creates the Request packet for the #write command
      #
      # @param data [String] the data to write to the file
      # @param offset [Integer] the offset in the file to start writing from
      # @param credit_charge [Integer] the number of credits that this request consumes
      # @return []RubySMB::SMB2::Packet::WriteRequest] the request packet
      def write_packet(data:'', offset: 0, credit_charge: 1)
        write_request               = set_header_fields(RubySMB::SMB2::Packet::WriteRequest.new)
        write_request.write_offset  = offset
        write_request.buffer        = data
        write_request.smb2_header.credit_charge = credit_charge
        write_request
      end

      def send_recv_write(data:'', offset: 0)
        pkt = write_packet(data: data, offset: offset)
        raw_response = tree.client.send_recv(pkt, encrypt: @tree_connect_encrypt_data)
        response = RubySMB::SMB2::Packet::WriteResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::WriteResponse::COMMAND,
            packet:         response
          )
        end
        unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
          raise RubySMB::Error::UnexpectedStatusCode, response.status_code
        end
        response.write_count
      end

      # Rename a file
      #
      # @param new_file_name [String] the new name
      # @return [WindowsError::ErrorCode] the NTStatus Response code
      # @raise [RubySMB::Error::InvalidPacket] if the response is not a SetInfoResponse packet
      def rename(new_file_name)
        raw_response = tree.client.send_recv(rename_packet(new_file_name), encrypt: @tree_connect_encrypt_data)
        response = RubySMB::SMB2::Packet::SetInfoResponse.read(raw_response)
        unless response.valid?
          raise RubySMB::Error::InvalidPacket.new(
            expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
            expected_cmd:   RubySMB::SMB2::Packet::SetInfoResponse::COMMAND,
            packet:         response
          )
        end
        response.smb2_header.nt_status.to_nt_status
      end

      # Crafts the SetInfoRequest packet to be sent for rename operations.
      #
      # @param new_file_name [String] the new name
      # @return [RubySMB::SMB2::Packet::SetInfoRequest] the set info packet
      def rename_packet(new_file_name)
        rename_request                  = set_header_fields(RubySMB::SMB2::Packet::SetInfoRequest.new)
        rename_request.file_info_class  = RubySMB::Fscc::FileInformation::FILE_RENAME_INFORMATION
        rename_request.buffer.file_name = new_file_name.encode('utf-16le')
        rename_request
      end

    end
  end
end