require 'spec_helper' require 'securerandom' RSpec.describe RubySMB::SMB2::File do let(:sock) { double('Socket', peeraddr: '192.168.1.5') } let(:dispatcher) { RubySMB::Dispatcher::Socket.new(sock) } let(:client) { RubySMB::Client.new(dispatcher, username: 'msfadmin', password: 'msfadmin') } let(:tree_id) { 2049 } let(:path) { '\\192.168.1.1\example' } let(:connect_response) { packet = RubySMB::SMB2::Packet::TreeConnectResponse.new packet.smb2_header.tree_id = tree_id packet.maximal_access.read("\xff\x01\x1f\x00") packet.share_type = 0x01 packet } let(:disco_req) { RubySMB::SMB2::Packet::TreeDisconnectRequest.new } let(:disco_resp) { RubySMB::SMB2::Packet::TreeDisconnectResponse.new } let(:tree) { RubySMB::SMB2::Tree.new(client: client, share: path, response: connect_response) } let(:file_id) { RubySMB::Field::Smb2Fileid.read('\x6d\x01\x00\x00\x00\x00\x00\x00\x\x01\x00\x00\x00\xff\xff\xff\xff') } let(:time) { DateTime.now } let(:create_response) { RubySMB::SMB2::Packet::CreateResponse.new( file_id: file_id, end_of_file: 108, allocation_size: 112, last_access: time, last_change: time, last_write: time ) } subject(:file) { described_class.new(name: 'short.txt', response: create_response, tree: tree) } it { is_expected.to respond_to :attributes } it { is_expected.to respond_to :guid } it { is_expected.to respond_to :last_access } it { is_expected.to respond_to :last_change } it { is_expected.to respond_to :last_write } it { is_expected.to respond_to :name } it { is_expected.to respond_to :size } it { is_expected.to respond_to :size_on_disk } it { is_expected.to respond_to :tree } it { is_expected.to respond_to :tree_connect_encrypt_data } it 'pulls the attributes from the response packet' do expect(file.attributes).to eq create_response.file_attributes end it 'pulls the GUID from the response fileid' do expect(file.guid).to eq create_response.file_id end it 'pulls the last access timestamps from the response packet' do expect(file.last_access).to eq create_response.last_access.to_datetime end it 'pulls the last change timestamps from the response packet' do expect(file.last_change).to eq create_response.last_change.to_datetime end it 'pulls the last write timestamps from the response packet' do expect(file.last_write).to eq create_response.last_write.to_datetime end it 'pulls the size from the response packet' do expect(file.size).to eq create_response.end_of_file end it 'pulls the size_on_disk from the response packet' do expect(file.size_on_disk).to eq create_response.allocation_size end it 'sets the tree_connect_encrypt_data flag to false by default' do expect(file.tree_connect_encrypt_data).to be false end describe '#set_header_fields' do let(:request) { RubySMB::SMB2::Packet::ReadRequest.new } it 'calls the set_header_field method from the Tree' do expect(tree).to receive(:set_header_fields).with(request).and_call_original file.set_header_fields(request) end it 'sets the packet file_id from the guid' do expect(file.set_header_fields(request).file_id).to eq file.guid end end describe '#read_packet' do it 'creates a new ReadRequest packet' do expect(RubySMB::SMB2::Packet::ReadRequest).to receive(:new).and_call_original file.read_packet end it 'calls #set_header_fields' do expect(file).to receive(:set_header_fields).and_call_original file.read_packet end it 'sets the read_length of the packet' do expect(file.read_packet(read_length: 55).read_length).to eq 55 end it 'sets the offset of the packet' do expect(file.read_packet(offset: 55).offset).to eq 55 end it 'sets the credit_charge of the packet' do expect(file.read_packet(credit_charge: 3).smb2_header.credit_charge).to eq 3 end end describe '#read' do context 'for a small file' do let(:small_read) { file.read_packet(read_length: 108) } let(:small_response) { response = RubySMB::SMB2::Packet::ReadResponse.new(data_length: 9, buffer: 'fake data') response.smb2_header.command = RubySMB::SMB2::Commands::READ response } before :example do allow(file).to receive(:read_packet) allow(client).to receive(:send_recv) allow(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).and_return(small_response) end it 'uses a single packet to read the entire file' do expect(file).to receive(:read_packet).with(read_length: 108, offset: 0, credit_charge: 0).and_return(small_read) expect(client).to receive(:send_recv).with(small_read, encrypt: false).and_return 'fake data' expect(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).with('fake data').and_return(small_response) expect(file.read).to eq 'fake data' end it 'calls Client #send_recv with encryption set if required' do file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(small_read, encrypt: true) file.read end context 'when the response is not valid' do it 'raise an InvalidPacket exception' do small_response.smb2_header.command = RubySMB::SMB2::Commands::LOGOFF expect { file.read }.to raise_error(RubySMB::Error::InvalidPacket) end end context 'when the response status code is not STATUS_SUCCESS' do it 'raise an UnexpectedStatusCode exception' do small_response.smb2_header.nt_status = WindowsError::NTStatus::STATUS_INVALID_HANDLE.value expect { file.read }.to raise_error(RubySMB::Error::UnexpectedStatusCode) end end end context 'for a larger file' do let(:big_read) { file.read_packet(read_length: 108) } let(:big_response) { response = RubySMB::SMB2::Packet::ReadResponse.new(data_length: 9, buffer: 'fake data') response.smb2_header.command = RubySMB::SMB2::Commands::READ response } before :example do client.server_supports_multi_credit = 1 allow(file).to receive(:read_packet) allow(client).to receive(:send_recv) allow(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).and_return(big_response) end it 'uses multiple packets to read the file in chunks' do expect(file).to receive(:read_packet).once.with(read_length: described_class::MAX_PACKET_SIZE, offset: 0, credit_charge: 1).and_return(big_read) expect(file).to receive(:read_packet).once.with(read_length: described_class::MAX_PACKET_SIZE, offset: described_class::MAX_PACKET_SIZE, credit_charge: 1).and_return(big_read) expect(client).to receive(:send_recv).twice.and_return 'fake data' expect(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).twice.with('fake data').and_return(big_response) file.read(bytes: (described_class::MAX_PACKET_SIZE * 2)) end it 'calls Client #send_recv with encryption set if required' do read_request = double('Read Request') allow(file).to receive(:read_packet).and_return(read_request) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).twice.with(read_request, encrypt: true) file.read(bytes: (described_class::MAX_PACKET_SIZE * 2)) end context 'when the second response is not valid' do it 'raise an InvalidPacket exception' do allow(file).to receive(:read_packet).with(read_length: described_class::MAX_PACKET_SIZE, offset: described_class::MAX_PACKET_SIZE, credit_charge: 1) do big_response.smb2_header.command = RubySMB::SMB2::Commands::LOGOFF end expect { file.read(bytes: (described_class::MAX_PACKET_SIZE * 2)) }.to raise_error(RubySMB::Error::InvalidPacket) end end context 'when the second response status code is not STATUS_SUCCESS' do it 'raise an UnexpectedStatusCode exception' do allow(file).to receive(:read_packet).with(read_length: described_class::MAX_PACKET_SIZE, offset: described_class::MAX_PACKET_SIZE, credit_charge: 1) do big_response.smb2_header.nt_status = WindowsError::NTStatus::STATUS_INVALID_HANDLE.value end expect { file.read(bytes: (described_class::MAX_PACKET_SIZE * 2)) }.to raise_error(RubySMB::Error::UnexpectedStatusCode) end end context 'when the server does not support multi credits' do it 'reads 65536 bytes at a time with no credit charge' do client.server_supports_multi_credit = false expect(file).to receive(:read_packet).once.with(read_length: 65536, offset: 0, credit_charge: 0).and_return(big_read) expect(file).to receive(:read_packet).once.with(read_length: 65536, offset: 65536, credit_charge: 0).and_return(big_read) file.read(bytes: (described_class::MAX_PACKET_SIZE * 4)) end end context 'when the server supports multi credits' do it 'reads a number of bytes equal to #server_max_read_size, with the expected credit charge' do credit_charge = (90000 - 1) / 65536 + 1 client.server_max_read_size = 90000 expect(file).to receive(:read_packet).once.with(read_length: 90000, offset: 0, credit_charge: credit_charge).and_return(big_read) expect(file).to receive(:read_packet).once.with(read_length: (described_class::MAX_PACKET_SIZE * 4 - 90000), offset: 90000, credit_charge: credit_charge).and_return(big_read) file.read(bytes: (described_class::MAX_PACKET_SIZE * 4)) end end end end describe '#append' do it 'call #write with offset set to the end of the file' do expect(file).to receive(:write).with(data:'test', offset: file.size) file.append(data:'test') end end describe '#write_packet' do it 'calls #set_header_fields with the newly created packet' do expect(file).to receive(:set_header_fields).and_call_original file.write_packet end it 'sets the offset on the packet' do expect(file.write_packet(offset:5).write_offset).to eq 5 end it 'sets the buffer on the packet' do expect(file.write_packet(data:'hello').buffer).to eq 'hello' end it 'sets the credit_charge on the packet header' do expect(file.write_packet(credit_charge: 5).smb2_header.credit_charge).to eq 5 end end describe '#write' do let(:write_response) { RubySMB::SMB2::Packet::WriteResponse.new } context 'for a small write' do it 'sends a single packet' do expect(client).to receive(:send_recv).once.and_return(write_response.to_binary_s) file.write(data: 'test') end it 'calls Client #send_recv with encryption set if required' do write_request = double('Write Request') allow(file).to receive(:write_packet).and_return(write_request) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).once.with(write_request, encrypt: true).and_return(write_response.to_binary_s) file.write(data: 'test') end end context 'for a large write' do before :example do allow(client).to receive(:send_recv).and_return(write_response.to_binary_s) client.server_supports_multi_credit = 1 end it 'sends multiple packets' do expect(client).to receive(:send_recv).twice.and_return(write_response.to_binary_s) file.write(data: SecureRandom.random_bytes(described_class::MAX_PACKET_SIZE + 1)) end it 'calls Client #send_recv with encryption set if required' do write_request = double('Write Request') allow(file).to receive(:write_packet).and_return(write_request) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).twice.with(write_request, encrypt: true).and_return(write_response.to_binary_s) file.write(data: SecureRandom.random_bytes(described_class::MAX_PACKET_SIZE + 1)) end context 'when the server does not support multi credits' do it 'writes 65536 bytes at a time with no credit charge' do client.server_supports_multi_credit = false data = SecureRandom.random_bytes(65536 * 2) expect(file).to receive(:write_packet).once.with(data: data[0, 65536], offset: 0, credit_charge: 0) expect(file).to receive(:write_packet).once.with(data: data[65536, (data.size - 65536)], offset: 65536, credit_charge: 0) file.write(data: data) end end context 'when the server supports multi credits' do it 'reads a number of bytes equal to #server_max_write_size, with the expected credit charge' do data = SecureRandom.random_bytes(90000 * 2) credit_charge = (90000 - 1) / 65536 + 1 client.server_max_write_size = 90000 expect(file).to receive(:write_packet).once.with(data: data[0, 90000], offset: 0, credit_charge: credit_charge) expect(file).to receive(:write_packet).once.with(data: data[90000, (data.size - 90000)], offset: 90000, credit_charge: credit_charge) file.write(data: data) end end end it 'raises an InvalidPacket exception if the response is not valid' do allow(client).to receive(:send_recv) allow(RubySMB::SMB2::Packet::WriteResponse).to receive(:read).and_return(write_response) allow(write_response).to receive(:valid?).and_return(false) expect { file.write(data: 'test') }.to raise_error(RubySMB::Error::InvalidPacket) end end describe '#delete_packet' do it 'creates a new SetInfoRequest packet' do expect(RubySMB::SMB2::Packet::SetInfoRequest).to receive(:new).and_call_original file.delete_packet end it 'calls #set_header_fields' do expect(file).to receive(:set_header_fields).and_call_original file.delete_packet end it 'sets the file_info_class of the packet' do expect(file.delete_packet.file_info_class).to eq RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION end it 'sets the delete_pending field to 1' do expect(file.delete_packet.buffer.delete_pending).to eq 1 end end describe '#delete' do context 'for a small file' do let(:small_delete) { file.delete_packet } let(:small_response) { RubySMB::SMB2::Packet::SetInfoResponse.new } it 'uses a single packet to delete the entire file' do expect(file).to receive(:delete_packet).and_return(small_delete) expect(client).to receive(:send_recv).with(small_delete, encrypt: false).and_return 'raw_response' expect(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).with('raw_response').and_return(small_response) expect(file.delete).to eq WindowsError::NTStatus::STATUS_SUCCESS end it 'raises an InvalidPacket exception if the response is not valid' do allow(file).to receive(:delete_packet) allow(client).to receive(:send_recv) allow(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).and_return(small_response) allow(small_response).to receive(:valid?).and_return(false) expect { file.delete }.to raise_error(RubySMB::Error::InvalidPacket) end it 'calls Client #send_recv with encryption set if required' do allow(file).to receive(:delete_packet) allow(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).and_return(small_response) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(small_delete, encrypt: true) file.delete end end end describe '#rename_packet' do it 'creates a new SetInfoRequest packet' do expect(RubySMB::SMB2::Packet::SetInfoRequest).to receive(:new).and_call_original file.rename_packet('new_file.txt') end it 'calls #set_header_fields' do expect(file).to receive(:set_header_fields).and_call_original file.rename_packet('new_file.txt') end it 'sets the file_info_class of the packet' do expect(file.rename_packet('new_file.txt').file_info_class).to eq RubySMB::Fscc::FileInformation::FILE_RENAME_INFORMATION end it 'sets the file_name field to the unicode-encoded new file name' do filename = "new_file.txt" expect(file.rename_packet(filename).buffer.file_name).to eq filename.encode('UTF-16LE').force_encoding('ASCII-8BIT') end end describe '#rename' do context 'for a small file' do let(:small_rename) { file.rename_packet('new_file.txt') } let(:small_response) { RubySMB::SMB2::Packet::SetInfoResponse.new } it 'uses a single packet to rename the entire file' do expect(file).to receive(:rename_packet).and_return(small_rename) expect(client).to receive(:send_recv).with(small_rename, encrypt: false).and_return 'raw_response' expect(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).with('raw_response').and_return(small_response) expect(file.rename('new_file.txt')).to eq WindowsError::NTStatus::STATUS_SUCCESS end it 'calls Client #send_recv with encryption set if required' do allow(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).and_return(small_response) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(small_rename, encrypt: true) file.rename('new_file.txt') end it 'raises an InvalidPacket exception if the response is not valid' do allow(client).to receive(:send_recv) allow(RubySMB::SMB2::Packet::SetInfoResponse).to receive(:read).and_return(small_response) allow(small_response).to receive(:valid?).and_return(false) expect { file.rename('new_file.txt') }.to raise_error(RubySMB::Error::InvalidPacket) end end end describe '#close' do let(:request) { double('CloseRequest') } let(:response) { double('CloseResponse') } let(:raw_response) { double('Raw response') } before :example do allow(RubySMB::SMB2::Packet::CloseRequest).to receive(:new).and_return(request) allow(file).to receive(:set_header_fields).and_return(request) allow(client).to receive(:send_recv).and_return(raw_response) allow(RubySMB::SMB2::Packet::CloseResponse).to receive(:read).and_return(response) allow(response).to receive(:valid?).and_return(true) allow(response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_SUCCESS) end it 'creates a new SMB2 CloseRequest packet' do expect(RubySMB::SMB2::Packet::CloseRequest).to receive(:new) file.close end it 'calls Tree #set_header_fields to set SetFileInformationRequest headers' do expect(file).to receive(:set_header_fields).with(request) file.close end it 'calls Client #send_recv with the expected request' do expect(client).to receive(:send_recv).with(request, encrypt: false) file.close end it 'calls Client #send_recv with encryption set if required' do file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(request, encrypt: true) file.close end it 'parses the response as a SMB2 CloseResponse packet' do expect(RubySMB::SMB2::Packet::CloseResponse).to receive(:read).with(raw_response) file.close end it 'raises an InvalidPacket exception if the response is not valid' do allow(response).to receive(:valid?).and_return(false) allow(response).to receive(:packet_smb_version) expect { file.close }.to raise_error(RubySMB::Error::InvalidPacket) end it 'raises an UnexpectedStatusCode exception if the response status code is not STATUS_SUCCESS' do allow(response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_OBJECT_NAME_NOT_FOUND) expect { file.close }.to raise_error(RubySMB::Error::UnexpectedStatusCode) end it 'returns the response status code' do expect(file.close).to eq WindowsError::NTStatus::STATUS_SUCCESS end end describe '#send_recv_read' do let(:read_data) { 'read data' } let(:raw_response) { double('fake raw response data') } let(:read_response) { res = RubySMB::SMB2::Packet::ReadResponse.new res.data_length = read_data.size res.buffer = read_data res } before :example do allow(client).to receive(:send_recv).and_return(raw_response) allow(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).with(raw_response).and_return(read_response) end context 'when the number of bytes to read is not provided' do it 'reads 0 bytes by default' do expect(file).to receive(:read_packet).with(read_length: 0, offset: 0).once.and_call_original file.send_recv_read end end it 'only reads the number of bytes provided as argument' do bytes = 5 expect(file).to receive(:read_packet).with(read_length: bytes, offset: 0).once.and_call_original file.send_recv_read(read_length: bytes) end it 'reads from the offset provided as argument' do offset = 3 expect(file).to receive(:read_packet).with(read_length: 0, offset: offset).once.and_call_original file.send_recv_read(offset: offset) end it 'calls Client #send_recv with the expected request' do request = double('Request') allow(file).to receive(:read_packet).and_return(request) expect(client).to receive(:send_recv).with(request, encrypt: false) file.send_recv_read end it 'calls Client #send_recv with encryption set if required' do request = double('Request') allow(file).to receive(:read_packet).and_return(request) file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(request, encrypt: true) file.send_recv_read end it 'parses the response as a SMB2 ReadResponse packet' do expect(RubySMB::SMB2::Packet::ReadResponse).to receive(:read).with(raw_response) file.send_recv_read end it 'raises an InvalidPacket exception if the response is not valid' do allow(read_response).to receive(:valid?).and_return(false) expect { file.send_recv_read }.to raise_error(RubySMB::Error::InvalidPacket) end it 'raises an UnexpectedStatusCode exception if the response status code is not STATUS_SUCCESS' do allow(read_response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_OBJECT_NAME_NOT_FOUND) expect { file.send_recv_read }.to raise_error(RubySMB::Error::UnexpectedStatusCode) end it 'returns the expected string' do expect(file.send_recv_read).to eq(read_data) end end describe '#send_recv_write' do let(:write_data) { 'write data' } let(:request) { double('Request') } let(:raw_response) { double('fake raw response data') } let(:write_response) { res = RubySMB::SMB2::Packet::WriteResponse.new res.write_count = write_data.size res } before :example do allow(file).to receive(:write_packet).and_return(request) allow(client).to receive(:send_recv).and_return(raw_response, encrypt: false) allow(RubySMB::SMB2::Packet::WriteResponse).to receive(:read).with(raw_response).and_return(write_response) end it 'reads 0 bytes from offset 0 by default' do expect(file).to receive(:write_packet).with(data: '', offset: 0).once.and_call_original file.send_recv_write end it 'writes the data provided as argument' do expect(file).to receive(:write_packet).with(data: write_data, offset: 0).once.and_call_original file.send_recv_write(data: write_data) end it 'reads from the offset provided as argument' do offset = 3 expect(file).to receive(:write_packet).with(data: '', offset: offset).once.and_call_original file.send_recv_write(offset: offset) end it 'calls Client #send_recv with the expected request' do expect(client).to receive(:send_recv).with(request, encrypt: false) file.send_recv_write end it 'calls Client #send_recv with encryption set if required' do file.tree_connect_encrypt_data = true expect(client).to receive(:send_recv).with(request, encrypt: true) file.send_recv_write end it 'parses the response as a SMB1 WriteResponse packet' do expect(RubySMB::SMB2::Packet::WriteResponse).to receive(:read).with(raw_response) file.send_recv_write end it 'raises an InvalidPacket exception if the response is not valid' do allow(write_response).to receive(:valid?).and_return(false) expect { file.send_recv_write }.to raise_error(RubySMB::Error::InvalidPacket) end it 'returns the expected response #write_count value' do expect(file.send_recv_write).to eq(write_data.size) end end end