lib/ruby_smb/smb1/file.rb
module RubySMB
module SMB1
# Represents a file on the Remote server that we can perform
# various I/O operations on.
class File
# The {RubySMB::SMB1::Tree} that this file belong to
# @!attribute [rw] tree
# @return [RubySMB::SMB1::Tree]
attr_accessor :tree
# The name of the file
# @!attribute [rw] name
# @return [String]
attr_accessor :name
# The {SmbExtFileAttributes} for the file
# @!attribute [rw] attributes
# @return [RubySMB::SMB1::BitField::SmbExtFileAttributes]
attr_accessor :attributes
# The file ID
# @!attribute [rw] fid
# @return [Integer]
attr_accessor :fid
# 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 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
def initialize(tree:, response:, name:)
raise ArgumentError, 'No tree provided' if tree.nil?
raise ArgumentError, 'No response provided' if response.nil?
raise ArgumentError, 'No file name provided' if name.nil?
@tree = tree
@name = name
@attributes = response.parameter_block.ext_file_attributes
@fid = response.parameter_block.fid
@last_access = response.parameter_block.last_access_time.to_datetime
@last_change = response.parameter_block.last_change_time.to_datetime
@last_write = response.parameter_block.last_write_time.to_datetime
@size = response.parameter_block.end_of_file
@size_on_disk = response.parameter_block.allocation_size
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 packet is not valid
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
def close
close_request = set_header_fields(RubySMB::SMB1::Packet::CloseRequest.new)
raw_response = @tree.client.send_recv(close_request)
response = RubySMB::SMB1::Packet::CloseResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::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 packet is not valid
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
def read(bytes: @size, offset: 0)
atomic_read_size = [bytes, @tree.client.max_buffer_size].min
remaining_bytes = bytes
data = ''
loop do
read_request = read_packet(read_length: atomic_read_size, offset: offset)
raw_response = @tree.client.send_recv(read_request)
response = RubySMB::SMB1::Packet::ReadAndxResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::ReadAndxResponse::COMMAND,
packet: response
)
end
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
end
if response.is_a?(RubySMB::SMB1::Packet::ReadAndxResponse)
data << response.data_block.data.to_binary_s
else
# Returns the current data immediately if we got an empty packet with an
# SMB_COM_READ_ANDX command and a STATUS_SUCCESS (just in case)
return data
end
remaining_bytes -= atomic_read_size
break unless remaining_bytes > 0
offset += atomic_read_size
atomic_read_size = remaining_bytes if remaining_bytes < @tree.client.max_buffer_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
# @return [RubySMB::SMB1::Packet::ReadAndxRequest] the crafted ReadRequest packet
def read_packet(read_length: 0, offset: 0)
read_request = set_header_fields(RubySMB::SMB1::Packet::ReadAndxRequest.new)
read_request.parameter_block.max_count_of_bytes_to_return = read_length
read_request.parameter_block.min_count_of_bytes_to_return = read_length
read_request.parameter_block.remaining = read_length
read_request.parameter_block.offset = offset
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)
response = RubySMB::SMB1::Packet::ReadAndxResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::ReadAndxResponse::COMMAND,
packet: response
)
end
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
end
response.data_block.data.to_binary_s
end
# Delete a file on close
#
# @return [WindowsError::ErrorCode] the NTStatus Response code
# @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid
def delete
raw_response = @tree.client.send_recv(delete_packet)
response = RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse::COMMAND,
packet: response
)
end
response.status_code
end
# Crafts the SetFileInformationRequest packet to be sent for delete operations.
#
# @return [RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest] the set info packet
def delete_packet
delete_request = RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest.new
delete_request = @tree.set_header_fields(delete_request)
delete_request.data_block.trans2_parameters.fid = @fid
passthrough_info_level = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION +
RubySMB::Fscc::FileInformation::SMB_INFO_PASSTHROUGH
delete_request.data_block.trans2_parameters.information_level = passthrough_info_level
delete_request.data_block.trans2_data.info_level_struct.delete_pending = 1
set_trans2_params(delete_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 [Integer] the count of bytes written
# @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
def write(data:, offset: 0)
buffer = data.dup
bytes = data.length
total_bytes_written = 0
loop do
atomic_write_size = [bytes, @tree.client.max_buffer_size].min
write_request = write_packet(data: buffer.slice!(0, atomic_write_size), offset: offset)
raw_response = @tree.client.send_recv(write_request)
response = RubySMB::SMB1::Packet::WriteAndxResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::WriteAndxResponse::COMMAND,
packet: response
)
end
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
end
bytes_written = response.parameter_block.count_low + (response.parameter_block.count_high << 16)
total_bytes_written += bytes_written
offset += bytes_written
bytes -= bytes_written
break unless buffer.length > 0
end
total_bytes_written
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
# @return [RubySMB::SMB1::Packet::WriteAndxRequest] the request packet
def write_packet(data:'', offset: 0)
write_request = set_header_fields(RubySMB::SMB1::Packet::WriteAndxRequest.new)
write_request.parameter_block.offset = offset
write_request.parameter_block.write_mode.writethrough_mode = 1
write_request.data_block.data = data
write_request.parameter_block.remaining = write_request.parameter_block.data_length
write_request
end
def send_recv_write(data:'', offset: 0)
pkt = write_packet(data: data, offset: offset)
pkt.set_64_bit_offset(true)
raw_response = @tree.client.send_recv(pkt)
response = RubySMB::SMB1::Packet::WriteAndxResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::WriteAndxResponse::COMMAND,
packet: response
)
end
response.parameter_block.count_low
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 packet is not valid
def rename(new_file_name)
raw_response = tree.client.send_recv(rename_packet(new_file_name))
response = RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse.read(raw_response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse::COMMAND,
packet: response
)
end
response.status_code
end
# Crafts the SetFileInformationRequest packet to be sent for rename operations.
#
# @param new_file_name [String] the new name
# @return [RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest] the set info packet
def rename_packet(new_file_name)
rename_request = RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest.new
rename_request = @tree.set_header_fields(rename_request)
rename_request.data_block.trans2_parameters.fid = @fid
passthrough_info_level = RubySMB::Fscc::FileInformation::FILE_RENAME_INFORMATION +
RubySMB::Fscc::FileInformation::SMB_INFO_PASSTHROUGH
rename_request.data_block.trans2_parameters.information_level = passthrough_info_level
rename_request.data_block.trans2_data.info_level_struct.file_name = new_file_name
set_trans2_params(rename_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 modified request packet
def set_header_fields(request)
request = @tree.set_header_fields(request)
request.parameter_block.fid = @fid
request
end
# Sets ParameterBlock options for Trans2 requests
def set_trans2_params(request)
request.parameter_block.total_parameter_count = request.parameter_block.parameter_count
request.parameter_block.total_data_count = request.parameter_block.data_count
request.parameter_block.max_parameter_count = request.parameter_block.parameter_count
request.parameter_block.max_data_count = 16_384
request
end
private :set_trans2_params
end
end
end