RSpec.describe RubySMB::SMB1::Pipe do

  it { expect(described_class).to be < RubySMB::SMB1::File }

  let(:peek_nmpipe_response) {
    packet = RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse.new
    packet.data_block.trans_parameters.read("\x10\x20\x00\x00\x03\x00")
    packet
  }
  let(:dispatcher) { RubySMB::Dispatcher::Socket.new(double('socket')) }
  let(:client) { RubySMB::Client.new(dispatcher, username: 'msfadmin', password: 'msfadmin') }
  let(:connect_response) {
    packet = RubySMB::SMB1::Packet::TreeConnectResponse.new
    packet.smb_header.tid = 2051
    packet.parameter_block.guest_access_rights.read("\xff\x01\x1f\x00")
    packet.parameter_block.access_rights.read("\xff\x01\x1f\x01")
    packet
  }
  let(:tree) { RubySMB::SMB1::Tree.new(client: client, share: '\\1.2.3.4\IPC$', response: connect_response) }
  let(:nt_create_andx_response) {
    response = RubySMB::SMB1::Packet::NtCreateAndxResponse.new
    response.parameter_block.ext_file_attributes = { normal: 1 }
    response.parameter_block.fid = 0x4000
    response.parameter_block.last_access_time = DateTime.parse("2017-09-20T1:1:1")
    response.parameter_block.last_change_time = DateTime.parse("2017-09-22T2:2:2")
    response.parameter_block.last_write_time  = DateTime.parse("2017-09-25T3:3:3")
    response.parameter_block.end_of_file = 53
    response.parameter_block.allocation_size = 4096
    response
  }
  let(:filename) { 'msf-pipe' }

  subject(:pipe) {
    described_class.new(tree: tree, response: nt_create_andx_response, name: filename)
  }

  describe '#peek' do
    let(:request) { RubySMB::SMB1::Packet::Trans::PeekNmpipeRequest.new }
    let(:raw_response) { double('Raw response') }
    let(:response) { double('Response') }

    before :example do
      allow(RubySMB::SMB1::Packet::Trans::PeekNmpipeRequest).to receive(:new).and_return(request)
      allow(client).to receive(:send_recv).and_return(raw_response)
      allow(RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse).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 PeekNmpipeRequest'do
      expect(RubySMB::SMB1::Packet::Trans::PeekNmpipeRequest).to receive(:new)
      pipe.peek
    end

    it 'sets the request #fid field' do
      expect(request).to receive(:fid=).with(pipe.fid)
      pipe.peek
    end

    it 'sets the request #max_data_count fieldto the peek_size argument' do
      peek_size = 5
      pipe.peek(peek_size: peek_size)
      expect(request.parameter_block.max_data_count).to eq(peek_size)
    end

    it 'calls Tree #set_header_fields' do
      expect(tree).to receive(:set_header_fields).with(request)
      pipe.peek
    end

    it 'calls Client #send_recv' do
      expect(client).to receive(:send_recv).with(request)
      pipe.peek
    end

    it 'parses the response as a SMB1 PeekNmpipeResponse packet' do
      expect(RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse).to receive(:read).with(raw_response)
      pipe.peek
    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 { pipe.peek }.to raise_error(RubySMB::Error::InvalidPacket)
    end

    it 'raises an UnexpectedStatusCode exception if the response status code is not STATUS_SUCCESS or STATUS_BUFFER_OVERFLOW' do
      allow(response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_OBJECT_NAME_NOT_FOUND)
      expect { pipe.peek }.to raise_error(RubySMB::Error::UnexpectedStatusCode)
    end

    it 'returns the expected response' do
      expect(pipe.peek).to eq(response)
    end
  end

  describe '#peek_available' do
    it 'reads the correct number of bytes available' do
      allow(pipe).to receive(:peek) { peek_nmpipe_response }
      allow(pipe).to receive(:peek_available) { pipe.peek.data_block.trans_parameters.read_data_available }
      expect(pipe.peek_available).to eq(0x2010)
    end
  end

  describe '#peek_state' do
    it 'reads the correct state of the pipe' do
      allow(pipe).to receive(:peek) { peek_nmpipe_response }
      allow(pipe).to receive(:peek_state) { pipe.peek.data_block.trans_parameters.pipe_state }
      expect(pipe.peek_state).to eq(RubySMB::SMB1::Pipe::STATUS_OK)
    end
  end

  describe '#is_connected?' do
    it 'identifies that the pipe is connected from the status' do
      allow(pipe).to receive(:peek) { peek_nmpipe_response }
      allow(pipe).to receive(:peek_state) { pipe.peek.data_block.trans_parameters.pipe_state }
      allow(pipe).to receive(:is_connected?) { pipe.peek_state == RubySMB::SMB1::Pipe::STATUS_OK }
      expect(pipe.is_connected?).to eq(true)
    end
  end

  describe '#initialize' do
    context 'when name is not provided' do
      it 'raises an ArgumentError' do
        expect {
          described_class.new(tree: tree, response: nt_create_andx_response, name: nil)
        }.to raise_error(ArgumentError)
      end
    end

    it 'calls the superclass with the expected arguments' do
      expect(pipe.tree).to eq(tree)
      expect(pipe.name).to eq(filename)
      expect(pipe.attributes).to eq(nt_create_andx_response.parameter_block.ext_file_attributes)
      expect(pipe.fid).to eq(nt_create_andx_response.parameter_block.fid)
      expect(pipe.last_access).to eq(nt_create_andx_response.parameter_block.last_access_time.to_datetime)
      expect(pipe.last_change).to eq(nt_create_andx_response.parameter_block.last_change_time.to_datetime)
      expect(pipe.last_write).to eq(nt_create_andx_response.parameter_block.last_write_time.to_datetime)
      expect(pipe.size).to eq(nt_create_andx_response.parameter_block.end_of_file)
      expect(pipe.size_on_disk).to eq(nt_create_andx_response.parameter_block.allocation_size)
    end

    {
      netlogon: RubySMB::Dcerpc::Netlogon,
      srvsvc: RubySMB::Dcerpc::Srvsvc,
      svcctl: RubySMB::Dcerpc::Svcctl,
      winreg: RubySMB::Dcerpc::Winreg,
      samr: RubySMB::Dcerpc::Samr,
      wkssvc: RubySMB::Dcerpc::Wkssvc
    }.each do |endpoint, klass|
      context "with \'#{endpoint}\' filename" do
        it "extends #{klass} class" do
          pipe = described_class.new(tree: tree, response: nt_create_andx_response, name: endpoint.to_s)
          expect(pipe).to be_a klass
        end
      end

      context "with \'\\#{endpoint}\' filename" do
        it "extends #{klass} class" do
          pipe = described_class.new(tree: tree, response: nt_create_andx_response, name: "\\#{endpoint.to_s}")
          expect(pipe).to be_a klass
        end
      end
    end
  end

  describe '#dcerpc_request' do
    let(:options)                   { { host: '1.2.3.4' } }
    let(:stub_packet )              { RubySMB::Dcerpc::Winreg::OpenKeyRequest.new }
    let(:dcerpc_request)            { double('DCERPC Request') }
    let(:request_stub)              { double('Request stub') }
    let(:binary_dcerpc_request)     { double('Binary DCERPC Request') }
    let(:trans_nmpipe_request)      { double('TransactNmpipeRequest') }
    let(:trans_data)                { double('Trans data') }
    let(:trans_nmpipe_raw_response) { double('Trans nmpipe raw response') }
    let(:trans_nmpipe_response)     { double('TransactNmpipeResponse') }
    let(:raw_data)                  { double('Raw data') }
    let(:dcerpc_response)           { double('DCERPC Response') }
    let(:result)                    { 'Result' }

    before :example do
      allow(RubySMB::Dcerpc::Request).to receive(:new).and_return(dcerpc_request)
      allow(dcerpc_request).to receive_messages(
        :stub        => request_stub,
        :to_binary_s => binary_dcerpc_request
      )
      allow(request_stub).to receive(:read)
      allow(RubySMB::SMB1::Packet::Trans::TransactNmpipeRequest).to receive(:new).and_return(trans_nmpipe_request)
      allow(tree).to receive(:set_header_fields)
      allow(trans_nmpipe_request).to receive_message_chain(:data_block, :trans_data => trans_data)
      allow(trans_nmpipe_request).to receive(:set_fid)
      allow(trans_data).to receive(:write_data=)
      allow(client).to receive(:send_recv).and_return(trans_nmpipe_raw_response)
      allow(RubySMB::SMB1::Packet::Trans::TransactNmpipeResponse).to receive(:read).and_return(trans_nmpipe_response)
      allow(trans_nmpipe_response).to receive_messages(
        :valid?      => true,
        :status_code => WindowsError::NTStatus::STATUS_SUCCESS
      )
      allow(trans_nmpipe_response).to receive_message_chain(:data_block, :trans_data, :read_data, :to_binary_s => raw_data)
      allow(RubySMB::Dcerpc::Response).to receive(:read).and_return(dcerpc_response)
      allow(dcerpc_response).to receive_message_chain(:pdu_header, :ptype => RubySMB::Dcerpc::PTypes::RESPONSE)
      allow(dcerpc_response).to receive(:stub).and_return(result)
    end

    it 'creates a Request packet with the expected arguments' do
      pipe.dcerpc_request(stub_packet, options)
      expect(options).to eq( { host: '1.2.3.4', endpoint: 'Winreg' })
      expect(RubySMB::Dcerpc::Request).to have_received(:new).with({ opnum: stub_packet.opnum }, options)
    end

    it 'sets DCERPC request stub to the stub packet passed as argument' do
      pipe.dcerpc_request(stub_packet, options)
      expect(request_stub).to have_received(:read).with(stub_packet.to_binary_s)
    end

    it 'creates a Trans TransactNmpipeRequest packet' do
      pipe.dcerpc_request(stub_packet, options)
      expect(RubySMB::SMB1::Packet::Trans::TransactNmpipeRequest).to have_received(:new).with(options)
    end

    it 'calls Tree #set_header_fields' do
      pipe.dcerpc_request(stub_packet, options)
      expect(tree).to have_received(:set_header_fields).with(trans_nmpipe_request)
    end

    it 'calls TransactNmpipeRequest #set_fid' do
      pipe.dcerpc_request(stub_packet, options)
      expect(trans_nmpipe_request).to have_received(:set_fid).with(pipe.fid)
    end

    it 'sets the expected #write_data request property' do
      pipe.dcerpc_request(stub_packet, options)
      expect(trans_data).to have_received(:write_data=).with(binary_dcerpc_request)
    end

    it 'sends the expected request' do
      pipe.dcerpc_request(stub_packet, options)
      expect(client).to have_received(:send_recv).with(trans_nmpipe_request)
    end

    it 'creates a Trans TransactNmpipeResponse packet from the response' do
      pipe.dcerpc_request(stub_packet, options)
      expect(RubySMB::SMB1::Packet::Trans::TransactNmpipeResponse).to have_received(:read).with(trans_nmpipe_raw_response)
    end

    context 'when the response is not a Trans packet' do
      it 'raises an InvalidPacket exception' do
        allow(trans_nmpipe_response).to receive(:packet_smb_version)
        allow(trans_nmpipe_response).to receive(:valid?).and_return(false)
        expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end

    context 'when the response status code is not STATUS_SUCCESS or STATUS_BUFFER_OVERFLOW' do
      it 'raises an UnexpectedStatusCode exception' do
        allow(trans_nmpipe_response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_INVALID_HANDLE)
        expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Error::UnexpectedStatusCode)
      end
    end

    context 'when the response status code is STATUS_SUCCESS' do
      it 'does not raise any exception' do
        expect { pipe.dcerpc_request(stub_packet, options) }.not_to raise_error
      end

      it 'creates a DCERPC Response packet from the response' do
        pipe.dcerpc_request(stub_packet, options)
        expect(RubySMB::Dcerpc::Response).to have_received(:read).with(raw_data)
      end

      context 'when an IOError occurs while parsing the DCERPC response' do
        it 'raises an InvalidPacket exception' do
          allow(RubySMB::Dcerpc::Response).to receive(:read).and_raise(IOError)
          expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
        end
      end

      context 'when the response is not a DCERPC Response packet' do
        it 'raises an InvalidPacket exception' do
          allow(dcerpc_response).to receive_message_chain(:pdu_header, :ptype => RubySMB::Dcerpc::PTypes::FAULT)
          expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
        end
      end

      it 'returns the expected stub data' do
        expect(pipe.dcerpc_request(stub_packet, options)).to eq(result)
      end
    end

    context 'when the response status code is STATUS_BUFFER_OVERFLOW' do
      let(:data_count)     { 100 }
      let(:added_raw_data) { double('Added raw data') }
      before :example do
        allow(trans_nmpipe_response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_BUFFER_OVERFLOW)
        allow(trans_nmpipe_response).to receive_message_chain(:parameter_block, :data_count => data_count)
        allow(pipe).to receive(:read).and_return(added_raw_data)
        allow(raw_data).to receive(:<<)
        allow(dcerpc_response).to receive_message_chain(:pdu_header, :pfc_flags, :first_frag => 1)
        allow(dcerpc_response).to receive_message_chain(:pdu_header, :pfc_flags, :last_frag => 1)
      end

      it 'does not raise any exception' do
        expect { pipe.dcerpc_request(stub_packet, options) }.not_to raise_error
      end

      it 'reads the expected number of bytes and concatenate it the first response raw data' do
        pipe.dcerpc_request(stub_packet, options)
        expect(pipe).to have_received(:read).with(bytes: tree.client.max_buffer_size - data_count)
        expect(raw_data).to have_received(:<<).with(added_raw_data)
      end

      it 'creates a DCERPC Response packet from the updated raw data' do
        pipe.dcerpc_request(stub_packet, options)
        expect(RubySMB::Dcerpc::Response).to have_received(:read).with(raw_data)
      end

      context 'when an IOError occurs while parsing the DCERPC response' do
        it 'raises an InvalidPacket exception' do
          allow(RubySMB::Dcerpc::Response).to receive(:read).and_raise(IOError)
          expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
        end
      end

      context 'when the response is not a DCERPC Response packet' do
        it 'raises an InvalidPacket exception' do
          allow(dcerpc_response).to receive_message_chain(:pdu_header, :ptype => RubySMB::Dcerpc::PTypes::FAULT)
          expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
        end
      end

      context 'when the response is not the first fragment' do
        it 'raises an InvalidPacket exception' do
          allow(dcerpc_response).to receive_message_chain(:pdu_header, :pfc_flags, :first_frag => 0)
          expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
        end
      end

      context 'when the response is the last fragment' do
        it 'only reads the pipe once' do
          pipe.dcerpc_request(stub_packet, options)
          expect(RubySMB::Dcerpc::Response).to have_received(:read).once
        end

        it 'returns the expected stub data' do
          expect(pipe.dcerpc_request(stub_packet, options)).to eq(result)
        end
      end

      context 'when the response is not the last fragment' do
        let(:raw_data2)        { double('Raw data #2') }
        let(:dcerpc_response2) { double('DCERPC Response #2') }
        let(:result2)          { 'Result #2' }
        before :example do
          allow(dcerpc_response).to receive_message_chain(:pdu_header, :pfc_flags, :last_frag => 0)
          allow(pipe).to receive(:read).with(bytes: tree.client.max_buffer_size).and_return(raw_data2)
          allow(RubySMB::Dcerpc::Response).to receive(:read).with(raw_data2).and_return(dcerpc_response2)
          allow(dcerpc_response2).to receive_message_chain(:pdu_header, :ptype => RubySMB::Dcerpc::PTypes::RESPONSE)
          allow(dcerpc_response2).to receive_message_chain(:pdu_header, :pfc_flags, :last_frag => 1)
          allow(dcerpc_response2).to receive(:stub).and_return(result2)
        end

        it 'reads the expected number of bytes' do
          pipe.dcerpc_request(stub_packet, options)
          expect(pipe).to have_received(:read).with(bytes: tree.client.max_buffer_size)
        end

        it 'creates a DCERPC Response packet from the new raw data' do
          pipe.dcerpc_request(stub_packet, options)
          expect(RubySMB::Dcerpc::Response).to have_received(:read).with(raw_data2)
        end

        context 'when an IOError occurs while parsing the new DCERPC response' do
          it 'raises an InvalidPacket exception' do
            allow(RubySMB::Dcerpc::Response).to receive(:read).with(raw_data2).and_raise(IOError)
            expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
          end
        end

        context 'when the new response is not a DCERPC Response packet' do
          it 'raises an InvalidPacket exception' do
            allow(dcerpc_response2).to receive_message_chain(:pdu_header, :ptype => RubySMB::Dcerpc::PTypes::FAULT)
            expect { pipe.dcerpc_request(stub_packet, options) }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket)
          end
        end

        it 'returns the expected stub data' do
          expect(pipe.dcerpc_request(stub_packet, options)).to eq(result)
        end
      end
    end
  end
end
