Class: RubySMB::SMB2::File

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

Overview

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

Direct Known Subclasses

Pipe

Constant Summary collapse

MAX_PACKET_SIZE =

The maximum number of byte we want to read or write in a single packet.

32_768

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of File.

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ruby_smb/smb2/file.rb', line 60

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

Instance Attribute Details

#attributesRubySMB::Fscc::FileAttributes



13
14
15
# File 'lib/ruby_smb/smb2/file.rb', line 13

def attributes
  @attributes
end

#guidRubySMB::Field::Smb2FileId

Returns:

  • (RubySMB::Field::Smb2FileId)


18
19
20
# File 'lib/ruby_smb/smb2/file.rb', line 18

def guid
  @guid
end

#last_accessDateTime

Returns:

  • (DateTime)


23
24
25
# File 'lib/ruby_smb/smb2/file.rb', line 23

def last_access
  @last_access
end

#last_changeDateTime

Returns:

  • (DateTime)


28
29
30
# File 'lib/ruby_smb/smb2/file.rb', line 28

def last_change
  @last_change
end

#last_writeDateTime

Returns:

  • (DateTime)


33
34
35
# File 'lib/ruby_smb/smb2/file.rb', line 33

def last_write
  @last_write
end

#nameString

Returns:

  • (String)


38
39
40
# File 'lib/ruby_smb/smb2/file.rb', line 38

def name
  @name
end

#sizeInteger

Returns:

  • (Integer)


43
44
45
# File 'lib/ruby_smb/smb2/file.rb', line 43

def size
  @size
end

#size_on_diskInteger

Returns:

  • (Integer)


48
49
50
# File 'lib/ruby_smb/smb2/file.rb', line 48

def size_on_disk
  @size_on_disk
end

#treeRubySMB::SMB2::Tree

Returns:



53
54
55
# File 'lib/ruby_smb/smb2/file.rb', line 53

def tree
  @tree
end

#tree_connect_encrypt_dataBoolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/ruby_smb/smb2/file.rb', line 58

def tree_connect_encrypt_data
  @tree_connect_encrypt_data
end

Instance Method Details

#append(data: '') ⇒ WindowsError::ErrorCode

Appends the supplied data to the end of the file.

Parameters:

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

    the data to write to the file

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus code returned from the operation



81
82
83
# File 'lib/ruby_smb/smb2/file.rb', line 81

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:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/ruby_smb/smb2/file.rb', line 90

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

#deleteWindowsError::ErrorCode

Delete a file on close

Returns:

  • (WindowsError::ErrorCode)

    the NTStatus Response code

Raises:



202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/ruby_smb/smb2/file.rb', line 202

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

#delete_packetRubySMB::SMB2::Packet::SetInfoRequest

Crafts the SetInfoRequest packet to be sent for delete operations.

Returns:



218
219
220
221
222
223
# File 'lib/ruby_smb/smb2/file.rb', line 218

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

#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:



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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/ruby_smb/smb2/file.rb', line 116

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

#read_packet(read_length: 0, offset: 0, credit_charge: 1) ⇒ RubySMB::SMB2::Packet::ReadRequest

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

  • credit_charge (Integer) (defaults to: 1)

    the number of credits that this request consumes

Returns:



173
174
175
176
177
178
179
# File 'lib/ruby_smb/smb2/file.rb', line 173

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

#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:



308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/ruby_smb/smb2/file.rb', line 308

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

#rename_packet(new_file_name) ⇒ RubySMB::SMB2::Packet::SetInfoRequest

Crafts the SetInfoRequest packet to be sent for rename operations.

Parameters:

  • new_file_name (String)

    the new name

Returns:



325
326
327
328
329
330
# File 'lib/ruby_smb/smb2/file.rb', line 325

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

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



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/ruby_smb/smb2/file.rb', line 181

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

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



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/ruby_smb/smb2/file.rb', line 286

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

#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:



229
230
231
232
233
# File 'lib/ruby_smb/smb2/file.rb', line 229

def set_header_fields(request)
  request         = tree.set_header_fields(request)
  request.file_id = guid
  request
end

#write(data: '', offset: 0) ⇒ WindowsError::ErrorCode

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

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:

  • (WindowsError::ErrorCode)

    the NTStatus code returned from the operation

Raises:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/ruby_smb/smb2/file.rb', line 241

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

#write_packet(data: '', offset: 0, credit_charge: 1) ⇒ Object

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

  • credit_charge (Integer) (defaults to: 1)

    the number of credits that this request consumes

Returns:

  • []RubySMB::SMB2::Packet::WriteRequest] the request packet



278
279
280
281
282
283
284
# File 'lib/ruby_smb/smb2/file.rb', line 278

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