Class: RubySMB::SMB1::File

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_smb/smb1/file.rb

Overview

Represents a file on the Remote server that we can perform various I/O operations on.

Direct Known Subclasses

Pipe

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tree:, response:, name:) ⇒ File

Returns a new instance of File.

Raises:

  • (ArgumentError)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ruby_smb/smb1/file.rb', line 51

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

Instance Attribute Details

#attributesRubySMB::SMB1::BitField::SmbExtFileAttributes



19
20
21
# File 'lib/ruby_smb/smb1/file.rb', line 19

def attributes
  @attributes
end

#fidInteger

Returns:

  • (Integer)


24
25
26
# File 'lib/ruby_smb/smb1/file.rb', line 24

def fid
  @fid
end

#last_accessDateTime

Returns:

  • (DateTime)


29
30
31
# File 'lib/ruby_smb/smb1/file.rb', line 29

def last_access
  @last_access
end

#last_changeDateTime

Returns:

  • (DateTime)


34
35
36
# File 'lib/ruby_smb/smb1/file.rb', line 34

def last_change
  @last_change
end

#last_writeDateTime

Returns:

  • (DateTime)


39
40
41
# File 'lib/ruby_smb/smb1/file.rb', line 39

def last_write
  @last_write
end

#nameString

Returns:

  • (String)


14
15
16
# File 'lib/ruby_smb/smb1/file.rb', line 14

def name
  @name
end

#sizeInteger

Returns:

  • (Integer)


44
45
46
# File 'lib/ruby_smb/smb1/file.rb', line 44

def size
  @size
end

#size_on_diskInteger

Returns:

  • (Integer)


49
50
51
# File 'lib/ruby_smb/smb1/file.rb', line 49

def size_on_disk
  @size_on_disk
end

#treeRubySMB::SMB1::Tree

Returns:



9
10
11
# File 'lib/ruby_smb/smb1/file.rb', line 9

def tree
  @tree
end

Instance Method Details

#append(data:) ⇒ WindowsError::ErrorCode

Appends the supplied data to the end of the file.

Parameters:

  • data (String)

    the data to write to the file

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus code returned from the operation



72
73
74
# File 'lib/ruby_smb/smb1/file.rb', line 72

def append(data:)
  write(data: data, offset: @size)
end

#closeWindowsError::ErrorCode

Closes the handle to the remote file.

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus code returned by the operation

Raises:



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/ruby_smb/smb1/file.rb', line 81

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

#deleteWindowsError::ErrorCode

Delete a file on close

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus Response code

Raises:



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ruby_smb/smb1/file.rb', line 182

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

#delete_packetRubySMB::SMB1::Packet::Trans2::SetFileInformationRequest

Crafts the SetFileInformationRequest packet to be sent for delete operations.



198
199
200
201
202
203
204
205
206
207
# File 'lib/ruby_smb/smb1/file.rb', line 198

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

#read(bytes: @size, offset: 0) ⇒ String

Read from the file, a specific number of bytes from a specific offset. If no parameters are given it will read the entire file.

Parameters:

  • bytes (Integer) (defaults to: @size)

    the number of bytes to read

  • offset (Integer) (defaults to: 0)

    the byte offset in the file to start reading from

Returns:

  • (String)

    the data read from the file

Raises:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ruby_smb/smb1/file.rb', line 107

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

#read_packet(read_length: 0, offset: 0) ⇒ RubySMB::SMB1::Packet::ReadAndxRequest

Crafts the ReadRequest packet to be sent for read operations.

Parameters:

  • bytes (Integer)

    the number of bytes to read

  • offset (Integer) (defaults to: 0)

    the byte offset in the file to start reading from

Returns:



150
151
152
153
154
155
156
157
# File 'lib/ruby_smb/smb1/file.rb', line 150

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

#rename(new_file_name) ⇒ WindowsError::ErrorCode

Rename a file

Parameters:

  • new_file_name (String)

    the new name

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus Response code

Raises:



280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/ruby_smb/smb1/file.rb', line 280

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

#rename_packet(new_file_name) ⇒ RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest

Crafts the SetFileInformationRequest packet to be sent for rename operations.

Parameters:

  • new_file_name (String)

    the new name

Returns:



297
298
299
300
301
302
303
304
305
306
# File 'lib/ruby_smb/smb1/file.rb', line 297

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

#send_recv_read(read_length: 0, offset: 0) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/ruby_smb/smb1/file.rb', line 159

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

#send_recv_write(data: '', offset: 0) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/ruby_smb/smb1/file.rb', line 260

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

#set_header_fields(request) ⇒ RubySMB::GenericPacket

Sets the header fields that we have to set on every packet we send for File operations.

Parameters:

Returns:



313
314
315
316
317
# File 'lib/ruby_smb/smb1/file.rb', line 313

def set_header_fields(request)
  request = @tree.set_header_fields(request)
  request.parameter_block.fid = @fid
  request
end

#write(data:, offset: 0) ⇒ Integer

Write the supplied data to the file at the given offset.

Parameters:

  • data (String)

    the data to write to the file

  • offset (Integer) (defaults to: 0)

    the offset in the file to start writing from

Returns:

  • (Integer)

    the count of bytes written

Raises:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ruby_smb/smb1/file.rb', line 216

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

#write_packet(data: '', offset: 0) ⇒ RubySMB::SMB1::Packet::WriteAndxRequest

Creates the Request packet for the #write command

Parameters:

  • data (String) (defaults to: '')

    the data to write to the file

  • offset (Integer) (defaults to: 0)

    the offset in the file to start writing from

Returns:



251
252
253
254
255
256
257
258
# File 'lib/ruby_smb/smb1/file.rb', line 251

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