diff --git a/lib/puppet/network/http/webrick/rest.rb b/lib/puppet/network/http/webrick/rest.rb index ae0867dfb..1cad76806 100644 --- a/lib/puppet/network/http/webrick/rest.rb +++ b/lib/puppet/network/http/webrick/rest.rb @@ -1,104 +1,107 @@ require 'puppet/network/http/handler' require 'resolv' require 'webrick' require 'puppet/util/ssl' class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet include Puppet::Network::HTTP::Handler def self.mutex @mutex ||= Mutex.new end def initialize(server) raise ArgumentError, "server is required" unless server register([Puppet::Network::HTTP::API::V2.routes, Puppet::Network::HTTP::API::V1.routes]) super(server) end # Retrieve the request parameters, including authentication information. def params(request) params = request.query || {} params = Hash[params.collect do |key, value| all_values = value.list [key, all_values.length == 1 ? value : all_values] end] params = decode_params(params) params.merge(client_information(request)) end # WEBrick uses a service method to respond to requests. Simply delegate to # the handler response method. def service(request, response) self.class.mutex.synchronize do process(request, response) end end def headers(request) result = {} request.each do |k, v| result[k.downcase] = v end result end def http_method(request) request.request_method end def path(request) request.path end def body(request) request.body end def client_cert(request) if cert = request.client_cert cert = Puppet::SSL::Certificate.from_instance(cert) warn_if_near_expiration(cert) cert else nil end end # Set the specified format as the content type of the response. def set_content_type(response, format) response["content-type"] = format_to_mime(format) end def set_response(response, result, status = 200) response.status = status if status >= 200 and status != 304 response.body = result response["content-length"] = result.stat.size if result.is_a?(File) end + if RUBY_VERSION[0,3] == "1.8" + response["connection"] = 'close' + end end # Retrieve node/cert/ip information from the request object. def client_information(request) result = {} if peer = request.peeraddr and ip = peer[3] result[:ip] = ip end # If they have a certificate (which will almost always be true) # then we get the hostname from the cert, instead of via IP # info result[:authenticated] = false if cert = request.client_cert and cn = Puppet::Util::SSL.cn_from_subject(cert.subject) result[:node] = cn result[:authenticated] = true else result[:node] = resolve_node(result) end result end end diff --git a/spec/unit/network/http/webrick/rest_spec.rb b/spec/unit/network/http/webrick/rest_spec.rb index 36b2bcff9..239a2de23 100755 --- a/spec/unit/network/http/webrick/rest_spec.rb +++ b/spec/unit/network/http/webrick/rest_spec.rb @@ -1,231 +1,250 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http' require 'webrick' require 'puppet/network/http/webrick/rest' describe Puppet::Network::HTTP::WEBrickREST do it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::WEBrickREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end describe "when receiving a request" do before do @request = stub('webrick http request', :query => {}, :peeraddr => %w{eh boo host ip}, :client_cert => nil) @response = mock('webrick http response') @model_class = stub('indirected model class') @webrick = stub('webrick http server', :mount => true, :[] => {}) Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) @handler = Puppet::Network::HTTP::WEBrickREST.new(@webrick) end it "should delegate its :service method to its :process method" do @handler.expects(:process).with(@request, @response).returns "stuff" @handler.service(@request, @response).should == "stuff" end describe "#headers" do let(:fake_request) { {"Foo" => "bar", "BAZ" => "bam" } } it "should iterate over the request object using #each" do fake_request.expects(:each) @handler.headers(fake_request) end it "should return a hash with downcased header names" do result = @handler.headers(fake_request) result.should == fake_request.inject({}) { |m,(k,v)| m[k.downcase] = v; m } end end describe "when using the Handler interface" do it "should use the request method as the http method" do @request.expects(:request_method).returns "FOO" @handler.http_method(@request).should == "FOO" end it "should return the request path as the path" do @request.expects(:path).returns "/foo/bar" @handler.path(@request).should == "/foo/bar" end it "should return the request body as the body" do @request.expects(:body).returns "my body" @handler.body(@request).should == "my body" end it "should set the response's 'content-type' header when setting the content type" do @response.expects(:[]=).with("content-type", "text/html") @handler.set_content_type(@response, "text/html") end it "should set the status and body on the response when setting the response for a successful query" do @response.expects(:status=).with 200 @response.expects(:body=).with "mybody" + @response.expects(:[]=).with('connection', 'close') if RUBY_VERSION[0,3] == '1.8' @handler.set_response(@response, "mybody", 200) end it "serves a file" do stat = stub 'stat', :size => 100 @file = stub 'file', :stat => stat, :path => "/tmp/path" @file.stubs(:is_a?).with(File).returns(true) @response.expects(:[]=).with('content-length', 100) @response.expects(:status=).with 200 @response.expects(:body=).with @file + @response.expects(:[]=).with('connection', 'close') if RUBY_VERSION[0,3] == '1.8' @handler.set_response(@response, @file, 200) end it "should set the status and message on the response when setting the response for a failed query" do @response.expects(:status=).with 400 @response.expects(:body=).with "mybody" + @response.expects(:[]=).with('connection', 'close') if RUBY_VERSION[0,3] == '1.8' @handler.set_response(@response, "mybody", 400) end + + it "omits the 'Connection: close' header on ruby 1.9 and up", :if => RUBY_VERSION[0,3] != '1.8' do + @response.expects(:status=).with 200 + @response.expects(:body=).with "mybody" + @response.expects(:[]=).with('connection', 'close').never + + @handler.set_response(@response, "mybody", 200) + end + + it "closes the connection on ruby 1.8", :if => RUBY_VERSION[0,3] == '1.8' do + @response.expects(:status=).with 200 + @response.expects(:body=).with "mybody" + @response.expects(:[]=).with('connection', 'close') + + @handler.set_response(@response, "mybody", 200) + end end describe "and determining the request parameters" do def query_of(options) request = Puppet::Indirector::Request.new(:myind, :find, "my key", nil, options) WEBrick::HTTPUtils.parse_query(request.query_string.sub(/^\?/, '')) end def a_request_querying(query_data) @request.expects(:query).returns(query_of(query_data)) @request end def certificate_with_subject(subj) cert = OpenSSL::X509::Certificate.new cert.subject = OpenSSL::X509::Name.parse(subj) cert end it "has no parameters when there is no query string" do only_server_side_information = [:authenticated, :ip, :node] @request.stubs(:query).returns(nil) result = @handler.params(@request) result.keys.sort.should == only_server_side_information end it "should include the HTTP request parameters, with the keys as symbols" do request = a_request_querying("foo" => "baz", "bar" => "xyzzy") result = @handler.params(request) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should handle parameters with no value" do request = a_request_querying('foo' => "") result = @handler.params(request) result[:foo].should == "" end it "should convert the string 'true' to the boolean" do request = a_request_querying('foo' => "true") result = @handler.params(request) result[:foo].should == true end it "should convert the string 'false' to the boolean" do request = a_request_querying('foo' => "false") result = @handler.params(request) result[:foo].should == false end it "should reconstruct arrays" do request = a_request_querying('foo' => ["a", "b", "c"]) result = @handler.params(request) result[:foo].should == ["a", "b", "c"] end it "should convert values inside arrays into primitive types" do request = a_request_querying('foo' => ["true", "false", "1", "1.2"]) result = @handler.params(request) result[:foo].should == [true, false, 1, 1.2] end it "should YAML-load values that are YAML-encoded" do request = a_request_querying('foo' => YAML.dump(%w{one two})) result = @handler.params(request) result[:foo].should == %w{one two} end it "should YAML-load that are YAML-encoded" do request = a_request_querying('foo' => YAML.dump(%w{one two})) result = @handler.params(request) result[:foo].should == %w{one two} end it "should not allow clients to set the node via the request parameters" do request = a_request_querying("node" => "foo") @handler.stubs(:resolve_node) @handler.params(request)[:node].should be_nil end it "should not allow clients to set the IP via the request parameters" do request = a_request_querying("ip" => "foo") @handler.params(request)[:ip].should_not == "foo" end it "should pass the client's ip address to model find" do @request.stubs(:peeraddr).returns(%w{noidea dunno hostname ipaddress}) @handler.params(@request)[:ip].should == "ipaddress" end it "should set 'authenticated' to true if a certificate is present" do cert = stub 'cert', :subject => [%w{CN host.domain.com}] @request.stubs(:client_cert).returns cert @handler.params(@request)[:authenticated].should be_true end it "should set 'authenticated' to false if no certificate is present" do @request.stubs(:client_cert).returns nil @handler.params(@request)[:authenticated].should be_false end it "should pass the client's certificate name to model method if a certificate is present" do @request.stubs(:client_cert).returns(certificate_with_subject("/CN=host.domain.com")) @handler.params(@request)[:node].should == "host.domain.com" end it "should resolve the node name with an ip address look-up if no certificate is present" do @request.stubs(:client_cert).returns nil @handler.expects(:resolve_node).returns(:resolved_node) @handler.params(@request)[:node].should == :resolved_node end it "should resolve the node name with an ip address look-up if CN parsing fails" do @request.stubs(:client_cert).returns(certificate_with_subject("/C=company")) @handler.expects(:resolve_node).returns(:resolved_node) @handler.params(@request)[:node].should == :resolved_node end end end end