Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/spec/lib/rex/proto/http/client_spec.rb
Views: 11789
# -*- coding:binary -*-12# Note: Some of these tests require a failed3# connection to 127.0.0.1:1. If you have some crazy local4# firewall that is dropping packets to this, your tests5# might be slow.6RSpec.describe Rex::Proto::Http::Client do78class << self910# Set a standard excuse that indicates that the method11# under test needs to be first examined to figure out12# what's sane and what's not.13def excuse_lazy(test_method=nil)14ret = "need to determine pass/fail criteria"15test_method ? ret << " for #{test_method.inspect}" : ret16end1718# Complain about not having a "real" connection (can be mocked)19def excuse_needs_connection20"need to actually set up an HTTP server to test"21end2223# Complain about not having a real auth server (can be mocked)24def excuse_needs_auth25"need to set up an HTTP authentication challenger"26end2728end2930let(:ip) { "1.2.3.4" }3132subject(:cli) do33Rex::Proto::Http::Client.new(ip)34end3536describe "#set_config" do3738it "should respond to #set_config" do39expect(cli.set_config).to eq({})40end4142end4344it "should respond to initialize" do45expect(cli).to be46end4748it "should have a set of default instance variables" do49expect(cli.instance_variable_get(:@hostname)).to eq ip50expect(cli.instance_variable_get(:@port)).to eq 8051expect(cli.instance_variable_get(:@context)).to eq({})52expect(cli.instance_variable_get(:@ssl)).to be_falsey53expect(cli.instance_variable_get(:@proxies)).to be_nil54expect(cli.instance_variable_get(:@username)).to be_empty55expect(cli.instance_variable_get(:@password)).to be_empty56expect(cli.config).to be_a_kind_of Hash57end5859it "should produce a raw HTTP request" do60expect(cli.request_raw).to be_a_kind_of Rex::Proto::Http::ClientRequest61end6263it "should produce a CGI HTTP request" do64req = cli.request_cgi65expect(req).to be_a_kind_of Rex::Proto::Http::ClientRequest66end6768context "with authorization" do69subject(:cli) do70cli = Rex::Proto::Http::Client.new(ip)71cli.set_config({"authorization" => "Basic base64dstuffhere"})72cli73end74let(:user) { "user" }75let(:pass) { "pass" }76let(:base64) { ["user:pass"].pack('m').chomp }7778context "and an Authorization header" do79before do80cli.set_config({"headers" => { "Authorization" => "Basic #{base64}" } })81end82it "should have one Authorization header" do83req = cli.request_cgi84match = req.to_s.match("Authorization: Basic")85expect(match).to be86expect(match.length).to eq 187end88it "should prefer the value in the header" do89req = cli.request_cgi90match = req.to_s.match(/Authorization: Basic (.*)$/)91expect(match).to be92expect(match.captures.length).to eq 193expect(match.captures[0].chomp).to eq base6494end95end96end9798context "with credentials" do99subject(:cli) do100cli = Rex::Proto::Http::Client.new(ip)101cli102end103let(:first_response) {104"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nWWW-Authenticate: Basic realm=\"foo\"\r\n\r\n"105}106let(:authed_response) {107"HTTP/1.1 200 Ok\r\nContent-Length: 0\r\n\r\n"108}109let(:user) { "user" }110let(:pass) { "pass" }111112it "should not send creds on the first request in order to induce a 401" do113req = subject.request_cgi114expect(req.to_s).not_to match("Authorization:")115end116117it "should send creds after receiving a 401" do118conn = double119allow(conn).to receive(:put)120allow(conn).to receive(:peerinfo)121allow(conn).to receive(:shutdown)122allow(conn).to receive(:close)123allow(conn).to receive(:closed?).and_return(false)124125expect(conn).to receive(:get_once).and_return(first_response, authed_response)126expect(conn).to receive(:put) do |str_request|127expect(str_request).not_to include("Authorization")128nil129end130expect(conn).to receive(:put) do |str_request|131expect(str_request).to include("Authorization")132nil133end134135expect(cli).to receive(:_send_recv).twice.and_call_original136137allow(Rex::Socket::Tcp).to receive(:create).and_return(conn)138139opts = { "username" => user, "password" => pass}140req = cli.request_cgi(opts)141cli.send_recv(req)142143# Make sure it didn't modify the argument144expect(opts).to eq({ "username" => user, "password" => pass})145end146147end148149it "should attempt to connect to a server" do150this_cli = Rex::Proto::Http::Client.new("127.0.0.1", 1)151expect { this_cli.connect(1) }.to raise_error ::Rex::ConnectionRefused152end153154it "should be able to close a connection" do155expect(cli.close).to be_nil156end157158it "should send a request and receive a response", :skip => excuse_needs_connection do159160end161162it "should send a request and receive a response without auth handling", :skip => excuse_needs_connection do163164end165166it "should send a request", :skip => excuse_needs_connection do167168end169170it "should test for credentials" do171skip "Should actually respond to :has_creds" do172expect(cli).not_to have_creds173this_cli = described_class.new("127.0.0.1", 1, {}, false, nil, nil, "user1", "pass1" )174expect(this_cli).to have_creds175end176end177178it "should send authentication", :skip => excuse_needs_connection179180it "should produce a basic authentication header" do181u = "user1"182p = "pass1"183b64 = ["#{u}:#{p}"].pack("m*").strip184expect(cli.basic_auth_header("user1","pass1")).to eq "Basic #{b64}"185end186187it "should perform digest authentication", :skip => excuse_needs_auth do188189end190191it "should perform negotiate authentication", :skip => excuse_needs_auth do192193end194195it "should get a response", :skip => excuse_needs_connection do196197end198199it "should end a connection with a stop" do200expect(cli.stop).to be_nil201end202203it "should test if a connection is valid" do204expect(cli.conn?).to be_falsey205end206207it "should tell if pipelining is enabled" do208expect(cli).not_to be_pipelining209this_cli = Rex::Proto::Http::Client.new("127.0.0.1", 1)210this_cli.pipeline = true211expect(this_cli).to be_pipelining212end213214it "should respond to its various accessors" do215expect(cli).to respond_to :config216expect(cli).to respond_to :config_types217expect(cli).to respond_to :pipeline218expect(cli).to respond_to :local_host219expect(cli).to respond_to :local_port220expect(cli).to respond_to :conn221expect(cli).to respond_to :context222expect(cli).to respond_to :proxies223expect(cli).to respond_to :username224expect(cli).to respond_to :password225expect(cli).to respond_to :junk_pipeline226end227# Not super sure why these are protected...228# Me either...229# Same here...230it "should refuse access to its protected accessors" do231expect {cli.ssl}.to raise_error NoMethodError232expect {cli.ssl_version}.to raise_error NoMethodError233expect {cli.hostname}.to raise_error NoMethodError234expect {cli.port}.to raise_error NoMethodError235end236237context 'with vars_form_data' do238subject(:cli) do239cli = Rex::Proto::Http::Client.new(ip)240cli.config['data'] = ''241cli.config['method'] = 'POST'242cli243end244let(:file_path) do245::File.join(::Msf::Config.install_root, 'spec', 'file_fixtures', 'string_list.txt')246end247let(:file) do248::File.open(file_path, 'rb')249end250let(:mock_boundary_suffix) do251'MockBoundary1234'252end253before(:each) do254allow(Rex::Text).to receive(:rand_text_numeric).with(30).and_return(mock_boundary_suffix)255end256257it 'should not include any form boundary metadata by default' do258request = cli.request_cgi({ })259260expected = <<~EOF261POST / HTTP/1.1\r262Host: #{ip}\r263User-Agent: #{request.opts['agent']}\r264Content-Type: application/x-www-form-urlencoded\r265Content-Length: 0\r266\r267EOF268expect(request.to_s).to eq(expected)269end270it 'should parse field name and file object as data' do271vars_form_data = [272{ 'name' => 'field1', 'data' => file, 'content_type' => 'text/plain' }273]274request = cli.request_cgi({ 'vars_form_data' => vars_form_data })275# We are gsub'ing here as HttpClient does this gsub to non-binary file data276file_contents = file.read.gsub("\r", '').gsub("\n", "\r\n")277expected = <<~EOF278POST / HTTP/1.1\r279Host: #{ip}\r280User-Agent: #{request.opts['agent']}\r281Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r282Content-Length: 214\r283\r284-----------------------------MockBoundary1234\r285Content-Disposition: form-data; name="field1"; filename="string_list.txt"\r286Content-Type: text/plain\r287\r288#{file_contents}\r289-----------------------------MockBoundary1234--\r290EOF291expect(request.to_s).to eq(expected)292end293it 'should parse field name and binary file object as data' do294vars_form_data = [295{ 'name' => 'field1', 'data' => file, 'encoding' => 'binary' }296]297request = cli.request_cgi({ 'vars_form_data' => vars_form_data })298expected = <<~EOF299POST / HTTP/1.1\r300Host: #{ip}\r301User-Agent: #{request.opts['agent']}\r302Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r303Content-Length: 221\r304\r305-----------------------------MockBoundary1234\r306Content-Disposition: form-data; name="field1"; filename="string_list.txt"\r307Content-Transfer-Encoding: binary\r308\r309#{file.read}\r310-----------------------------MockBoundary1234--\r311EOF312expect(request.to_s).to eq(expected)313end314it 'should parse field name and binary file object as data with filename override' do315vars_form_data = [316{ 'name' => 'field1', 'data' => file, 'encoding' => 'binary', 'content_type' => 'text/plain', 'filename' => 'my_file.txt' }317]318request = cli.request_cgi({ 'vars_form_data' => vars_form_data })319expected = <<~EOF320POST / HTTP/1.1\r321Host: #{ip}\r322User-Agent: #{request.opts['agent']}\r323Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r324Content-Length: 243\r325\r326-----------------------------MockBoundary1234\r327Content-Disposition: form-data; name="field1"; filename="my_file.txt"\r328Content-Type: text/plain\r329Content-Transfer-Encoding: binary\r330\r331#{file.read}\r332-----------------------------MockBoundary1234--\r333EOF334expect(request.to_s).to eq(expected)335end336it 'should parse data correctly when provided with a string' do337data = 'hello world'338vars_form_data = [339{ 'name' => 'file1', 'data' => data, 'filename' => 'file1' }340]341request = cli.request_cgi({ 'vars_form_data' => vars_form_data })342expected = <<~EOF343POST / HTTP/1.1\r344Host: #{ip}\r345User-Agent: #{request.opts['agent']}\r346Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r347Content-Length: 175\r348\r349-----------------------------MockBoundary1234\r350Content-Disposition: form-data; name="file1"; filename="file1"\r351\r352#{data}\r353-----------------------------MockBoundary1234--\r354EOF355expect(request.to_s).to eq(expected)356end357it 'should parse data correctly when provided with a string and content type' do358data = 'hello world'359vars_form_data = [360{ 'name' => 'file1', 'data' => data, 'filename' => 'file1', 'content_type' => 'text/plain' }361]362request = cli.request_cgi({ 'vars_form_data' => vars_form_data })363expected = <<~EOF364POST / HTTP/1.1\r365Host: #{ip}\r366User-Agent: #{request.opts['agent']}\r367Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r368Content-Length: 201\r369\r370-----------------------------MockBoundary1234\r371Content-Disposition: form-data; name="file1"; filename="file1"\r372Content-Type: text/plain\r373\r374#{data}\r375-----------------------------MockBoundary1234--\r376EOF377expect(request.to_s).to eq(expected)378end379it 'should parse data correctly when provided with a string, content type and filename' do380data = 'hello world'381vars_form_data = [382{ 'name' => 'file1', 'data' => data, 'content_type' => 'text/plain', 'filename' => 'my_file.txt' }383]384request = cli.request_cgi({ 'vars_form_data' => vars_form_data })385expected = <<~EOF386POST / HTTP/1.1\r387Host: #{ip}\r388User-Agent: #{request.opts['agent']}\r389Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r390Content-Length: 207\r391\r392-----------------------------MockBoundary1234\r393Content-Disposition: form-data; name="file1"; filename="my_file.txt"\r394Content-Type: text/plain\r395\r396#{data}\r397-----------------------------MockBoundary1234--\r398EOF399expect(request.to_s).to eq(expected)400end401it 'should parse data correctly when provided with a number' do402data = 123403vars_form_data = [404{ 'name' => 'file1', 'data' => data, 'content_type' => 'text/plain' }405]406request = cli.request_cgi({ 'vars_form_data' => vars_form_data })407expected = <<~EOF408POST / HTTP/1.1\r409Host: #{ip}\r410User-Agent: #{request.opts['agent']}\r411Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r412Content-Length: 175\r413\r414-----------------------------MockBoundary1234\r415Content-Disposition: form-data; name="file1"\r416Content-Type: text/plain\r417\r418#{data}\r419-----------------------------MockBoundary1234--\r420EOF421expect(request.to_s).to eq(expected)422end423it 'should parse dat correctly when provided with an IO object' do424require 'stringio'425str = 'Hello World!'426vars_form_data = [427{ 'name' => 'file1', 'data' => ::StringIO.new(str), 'content_type' => 'text/plain', 'filename' => 'my_file.txt' }428]429request = cli.request_cgi({ 'vars_form_data' => vars_form_data })430expected = <<~EOF431POST / HTTP/1.1\r432Host: #{ip}\r433User-Agent: #{request.opts['agent']}\r434Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r435Content-Length: 208\r436\r437-----------------------------MockBoundary1234\r438Content-Disposition: form-data; name="file1"; filename="my_file.txt"\r439Content-Type: text/plain\r440\r441#{str}\r442-----------------------------MockBoundary1234--\r443EOF444expect(request.to_s).to eq(expected)445end446it 'should handle nil data values correctly' do447vars_form_data = [448{ 'name' => 'nil_value', 'data' => nil }449]450request = cli.request_cgi({ 'vars_form_data' => vars_form_data })451# This could potentially return one less '\r'.452expected = <<~EOF453POST / HTTP/1.1\r454Host: #{ip}\r455User-Agent: #{request.opts['agent']}\r456Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r457Content-Length: 150\r458\r459-----------------------------MockBoundary1234\r460Content-Disposition: form-data; name="nil_value"\r461\r462\r463-----------------------------MockBoundary1234--\r464EOF465expect(request.to_s).to eq(expected)466end467it 'should handle nil field values correctly' do468vars_form_data = [469{ 'name' => nil, 'data' => '123' },470{ 'data' => '456' },471]472request = cli.request_cgi({ 'vars_form_data' => vars_form_data })473expected = <<~EOF474POST / HTTP/1.1\r475Host: #{ip}\r476User-Agent: #{request.opts['agent']}\r477Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r478Content-Length: 221\r479\r480-----------------------------MockBoundary1234\r481Content-Disposition: form-data\r482\r483123\r484-----------------------------MockBoundary1234\r485Content-Disposition: form-data\r486\r487456\r488-----------------------------MockBoundary1234--\r489EOF490expect(request.to_s).to eq(expected)491end492it 'should handle nil field values and data correctly' do493vars_form_data = [494{ 'name' => nil, 'data' => nil }495]496request = cli.request_cgi({ 'vars_form_data' => vars_form_data })497expected = <<~EOF498POST / HTTP/1.1\r499Host: #{ip}\r500User-Agent: #{request.opts['agent']}\r501Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r502Content-Length: 132\r503\r504-----------------------------MockBoundary1234\r505Content-Disposition: form-data\r506\r507\r508-----------------------------MockBoundary1234--\r509EOF510expect(request.to_s).to eq(expected)511end512it 'should raise an error on non-string field names' do513invalid_names = [514false,515true,516123,517['hello'],518{ k: 'val' }519]520invalid_names.each do |name|521vars_form_data = [522{ 'name' => name, 'data' => '123' }523]524525request = cli.request_cgi({ 'vars_form_data' => vars_form_data })526expect { request.to_s }.to raise_error /The provided field `name` option is not valid. Expected: String/527end528end529530it 'should handle binary correctly' do531binary_data = (0..255).map { |x| x.chr }.join532vars_form_data = [533{ 'name' => 'field1', 'data' => binary_data, 'encoding' => 'binary' }534]535request = cli.request_cgi({ 'vars_form_data' => vars_form_data })536expected = <<~EOF537POST / HTTP/1.1\r538Host: #{ip}\r539User-Agent: #{request.opts['agent']}\r540Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r541Content-Length: 438\r542\r543-----------------------------MockBoundary1234\r544Content-Disposition: form-data; name="field1"\r545Content-Transfer-Encoding: binary\r546\r547#{binary_data}\r548-----------------------------MockBoundary1234--\r549EOF550expect(request.to_s).to eq(expected)551end552it 'should handle duplicate file and field names correctly' do553vars_form_data = [554{ 'name' => 'file', 'data' => 'file1_content', 'filename' => 'duplicate.txt' },555{ 'name' => 'file', 'data' => 'file2_content', 'filename' => 'duplicate.txt' },556{ 'name' => 'file', 'data' => 'file2_content', 'filename' => 'duplicate.txt' },557# Note, this won't actually attempt to read a file - the content will be set to 'file.txt'558{ 'name' => 'file', 'data' => 'file.txt', 'filename' => 'duplicate.txt' }559]560request = cli.request_cgi({ 'vars_form_data' => vars_form_data })561expected = <<~EOF562POST / HTTP/1.1\r563Host: #{ip}\r564User-Agent: #{request.opts['agent']}\r565Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r566Content-Length: 584\r567\r568-----------------------------MockBoundary1234\r569Content-Disposition: form-data; name="file"; filename="duplicate.txt"\r570\r571file1_content\r572-----------------------------MockBoundary1234\r573Content-Disposition: form-data; name="file"; filename="duplicate.txt"\r574\r575file2_content\r576-----------------------------MockBoundary1234\r577Content-Disposition: form-data; name="file"; filename="duplicate.txt"\r578\r579file2_content\r580-----------------------------MockBoundary1234\r581Content-Disposition: form-data; name="file"; filename="duplicate.txt"\r582\r583file.txt\r584-----------------------------MockBoundary1234--\r585EOF586expect(request.to_s).to eq(expected)587end588it 'does not encode special characters in file name by default as it may be used as part of an exploit' do589vars_form_data = [590{ 'name' => 'file', 'data' => 'abc', 'content_type' => 'text/plain', 'encoding' => '8bit', 'filename' => "'t \"e 'st.txt'" }591]592request = cli.request_cgi({ 'vars_form_data' => vars_form_data })593expected = <<~EOF594POST / HTTP/1.1\r595Host: #{ip}\r596User-Agent: #{request.opts['agent']}\r597Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r598Content-Length: 234\r599\r600-----------------------------MockBoundary1234\r601Content-Disposition: form-data; name="file"; filename="'t \"e 'st.txt'"\r602Content-Type: text/plain\r603Content-Transfer-Encoding: 8bit\r604\r605abc\r606-----------------------------MockBoundary1234--\r607EOF608expect(request.to_s).to eq(expected)609end610it 'should handle nil filename values correctly' do611vars_form_data = [612{ 'name' => 'example_name', 'data' => 'example_data', 'filename' => nil }613]614request = cli.request_cgi({ 'vars_form_data' => vars_form_data })615expected = <<~EOF616POST / HTTP/1.1\r617Host: #{ip}\r618User-Agent: #{request.opts['agent']}\r619Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r620Content-Length: 165\r621\r622-----------------------------MockBoundary1234\r623Content-Disposition: form-data; name="example_name"\r624\r625example_data\r626-----------------------------MockBoundary1234--\r627EOF628expect(request.to_s).to eq(expected)629end630it 'should handle nil encoding values correctly' do631vars_form_data = [632{ 'name' => 'example_name', 'data' => 'example_data', 'encoding' => nil }633]634request = cli.request_cgi({ 'vars_form_data' => vars_form_data })635expected = <<~EOF636POST / HTTP/1.1\r637Host: #{ip}\r638User-Agent: #{request.opts['agent']}\r639Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r640Content-Length: 165\r641\r642-----------------------------MockBoundary1234\r643Content-Disposition: form-data; name="example_name"\r644\r645example_data\r646-----------------------------MockBoundary1234--\r647EOF648expect(request.to_s).to eq(expected)649end650it 'should handle nil content type values correctly' do651vars_form_data = [652{ 'name' => 'example_name', 'data' => 'example_data', 'content_type' => nil }653]654request = cli.request_cgi({ 'vars_form_data' => vars_form_data })655expected = <<~EOF656POST / HTTP/1.1\r657Host: #{ip}\r658User-Agent: #{request.opts['agent']}\r659Content-Type: multipart/form-data; boundary=---------------------------MockBoundary1234\r660Content-Length: 165\r661\r662-----------------------------MockBoundary1234\r663Content-Disposition: form-data; name="example_name"\r664\r665example_data\r666-----------------------------MockBoundary1234--\r667EOF668expect(request.to_s).to eq(expected)669end670671it 'should not hang when parsing a HEAD response' do672response = <<~EOF673HTTP/1.1 200 OK674Date: Thu, 15 Dec 2022 02:52:42 GMT675Server: Apache676Expires: Thu, 19 Nov 1981 08:52:00 GMT677Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0678Pragma: no-cache679Vary: Accept-Encoding680Access-Control-Allow-Origin: *681Connection: close682Content-Type: text/html; charset=UTF-8683Content-Length: 1000684685686EOF687688conn = double689allow(conn).to receive(:put)690allow(conn).to receive(:peerinfo)691allow(conn).to receive(:closed?).and_return(false)692693expect(conn).to receive(:get_once).at_least(:once).and_return(response, nil)694allow(Rex::Socket::Tcp).to receive(:create).and_return(conn)695696request = cli.request_cgi('method' => 'HEAD')697resp = cli.send_recv(request,5)698699expect(resp.headers['Content-Length']).to eq('1000')700end701end702end703704705