diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb index 9fc1d08b3..87ef4fbbf 100644 --- a/lib/puppet/file_serving/content.rb +++ b/lib/puppet/file_serving/content.rb @@ -1,49 +1,50 @@ # # Created by Luke Kanies on 2007-10-16. # Copyright (c) 2007. All rights reserved. require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file contents. # It only reads the file when its content is specifically # asked for. class Puppet::FileServing::Content < Puppet::FileServing::Base extend Puppet::Indirector indirects :file_content, :extend => Puppet::FileServing::IndirectionHooks attr_writer :content def self.supported_formats [:raw] end def self.from_raw(content) instance = new("/this/is/a/fake/path") instance.content = content instance end - # Collect our data. + # BF: we used to fetch the file content here, but this is counter-productive + # for puppetmaster streaming of file content. So collect just returns itself def collect return if stat.ftype == "directory" - content + self end # Read the content of our file in. def content unless defined?(@content) and @content # This stat can raise an exception, too. raise(ArgumentError, "Cannot read the contents of links unless following links") if stat().ftype == "symlink" @content = ::File.read(full_path()) end @content end def to_raw - content + File.new(full_path(), "r") end end diff --git a/lib/puppet/network/http/mongrel/rest.rb b/lib/puppet/network/http/mongrel/rest.rb index 7b28d880b..8668bf802 100644 --- a/lib/puppet/network/http/mongrel/rest.rb +++ b/lib/puppet/network/http/mongrel/rest.rb @@ -1,85 +1,92 @@ require 'puppet/network/http/handler' class Puppet::Network::HTTP::MongrelREST < Mongrel::HttpHandler include Puppet::Network::HTTP::Handler ACCEPT_HEADER = "HTTP_ACCEPT".freeze # yay, zed's a crazy-man def initialize(args={}) super() initialize_for_puppet(args) end def accept_header(request) request.params[ACCEPT_HEADER] end def content_type_header(request) request.params["HTTP_CONTENT_TYPE"] end # which HTTP verb was used in this request def http_method(request) request.params[Mongrel::Const::REQUEST_METHOD] end # Return the query params for this request. We had to expose this method for # testing purposes. def params(request) params = Mongrel::HttpRequest.query_parse(request.params["QUERY_STRING"]) params = decode_params(params) params.merge(client_info(request)) end # what path was requested? def path(request) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] #x = '/' + request.params[Mongrel::Const::REQUEST_PATH] request.params[Mongrel::Const::REQUEST_PATH] end # return the request body def body(request) request.body.read end def set_content_type(response, format) response.header['Content-Type'] = format_to_mime(format) end # produce the body of the response def set_response(response, result, status = 200) # Set the 'reason' (or 'message', as it's called in Webrick), when # we have a failure, unless we're on a version of mongrel that doesn't # support this. if status < 300 - response.start(status) { |head, body| body.write(result) } + unless result.is_a?(File) + response.start(status) { |head, body| body.write(result) } + else + response.start(status) { |head, body| } + response.send_status(result.stat.size) + response.send_header + response.send_file(result.path) + end else begin response.start(status,false,result) { |head, body| body.write(result) } rescue ArgumentError - response.start(status) { |head, body| body.write(result) } + response.start(status) { |head, body| body.write(result) } end end end def client_info(request) result = {} params = request.params result[:ip] = params["HTTP_X_FORWARDED_FOR"] ? params["HTTP_X_FORWARDED_FOR"].split(',').last.strip : params["REMOTE_ADDR"] # JJM #906 The following dn.match regular expression is forgiving # enough to match the two Distinguished Name string contents # coming from Apache, Pound or other reverse SSL proxies. if dn = params[Puppet[:ssl_client_header]] and dn_matchdata = dn.match(/^.*?CN\s*=\s*(.*)/) result[:node] = dn_matchdata[1].to_str result[:authenticated] = (params[Puppet[:ssl_client_verify_header]] == 'SUCCESS') else result[:node] = resolve_node(result) result[:authenticated] = false end return result end end diff --git a/lib/puppet/network/http/rack/rest.rb b/lib/puppet/network/http/rack/rest.rb index 104751271..5245275dd 100644 --- a/lib/puppet/network/http/rack/rest.rb +++ b/lib/puppet/network/http/rack/rest.rb @@ -1,79 +1,102 @@ require 'puppet/network/http/handler' require 'puppet/network/http/rack/httphandler' class Puppet::Network::HTTP::RackREST < Puppet::Network::HTTP::RackHttpHandler include Puppet::Network::HTTP::Handler HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze ContentType = 'Content-Type'.freeze + CHUNK_SIZE = 8192 + + class RackFile + def initialize(file) + @file = file + end + + def each + while chunk = @file.read(CHUNK_SIZE) + yield chunk + end + end + + def close + @file.close + end + end + def initialize(args={}) super() initialize_for_puppet(args) end def set_content_type(response, format) response[ContentType] = format_to_mime(format) end # produce the body of the response def set_response(response, result, status = 200) response.status = status - response.write result + unless result.is_a?(File) + response.write result + else + response["Content-Length"] = result.stat.size + response.body = RackFile.new(result) + end end # Retrieve the accept header from the http request. def accept_header(request) request.env[HEADER_ACCEPT] end # Retrieve the accept header from the http request. def content_type_header(request) request.content_type end # Return which HTTP verb was used in this request. def http_method(request) request.request_method end # Return the query params for this request. def params(request) result = decode_params(request.params) result.merge(extract_client_info(request)) end # what path was requested? (this is, without any query parameters) def path(request) request.path end # return the request body # request.body has some limitiations, so we need to concat it back # into a regular string, which is something puppet can use. def body(request) body = '' request.body.each { |part| body += part } body end def extract_client_info(request) result = {} result[:ip] = request.ip # if we find SSL info in the headers, use them to get a hostname. # try this with :ssl_client_header, which defaults should work for # Apache with StdEnvVars. if dn = request.env[Puppet[:ssl_client_header]] and dn_matchdata = dn.match(/^.*?CN\s*=\s*(.*)/) result[:node] = dn_matchdata[1].to_str result[:authenticated] = (request.env[Puppet[:ssl_client_verify_header]] == 'SUCCESS') else result[:node] = resolve_node(result) result[:authenticated] = false end result end end diff --git a/lib/puppet/network/http/webrick/rest.rb b/lib/puppet/network/http/webrick/rest.rb index 274665dcd..9bfd0ce6a 100644 --- a/lib/puppet/network/http/webrick/rest.rb +++ b/lib/puppet/network/http/webrick/rest.rb @@ -1,77 +1,82 @@ require 'puppet/network/http/handler' require 'resolv' class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet include Puppet::Network::HTTP::Handler def initialize(server, handler) raise ArgumentError, "server is required" unless server super(server) initialize_for_puppet(:server => server, :handler => handler) end # Retrieve the request parameters, including authentication information. def params(request) result = request.query result = decode_params(result) result.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) process(request, response) end def accept_header(request) request["accept"] end def content_type_header(request) request["content-type"] end def http_method(request) request.request_method end def path(request) request.path end def body(request) request.body 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 - response.body = result if status >= 200 and status != 304 + if status >= 200 and status != 304 + response.body = result + if result.is_a?(File) + response["content-length"] = result.stat.size + end + end response.reason_phrase = result if status < 200 or status >= 300 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 nameary = cert.subject.to_a.find { |ary| ary[0] == "CN" } result[:node] = nameary[1] result[:authenticated] = true else result[:node] = resolve_node(result) end result end end diff --git a/spec/unit/file_serving/content.rb b/spec/unit/file_serving/content.rb index eacaff89f..cba703902 100755 --- a/spec/unit/file_serving/content.rb +++ b/spec/unit/file_serving/content.rb @@ -1,110 +1,118 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/file_serving/content' describe Puppet::FileServing::Content do it "should should be a subclass of Base" do Puppet::FileServing::Content.superclass.should equal(Puppet::FileServing::Base) end it "should indirect file_content" do Puppet::FileServing::Content.indirection.name.should == :file_content end it "should should include the IndirectionHooks module in its indirection" do Puppet::FileServing::Content.indirection.metaclass.included_modules.should include(Puppet::FileServing::IndirectionHooks) end it "should only support the raw format" do Puppet::FileServing::Content.supported_formats.should == [:raw] end it "should have a method for collecting its attributes" do Puppet::FileServing::Content.new("/path").should respond_to(:collect) end - it "should retrieve and store its contents when its attributes are collected if the file is a normal file" do + it "should not retrieve and store its contents when its attributes are collected if the file is a normal file" do content = Puppet::FileServing::Content.new("/path") result = "foo" File.stubs(:lstat).returns(stub("stat", :ftype => "file")) - File.expects(:read).with("/path").returns result + File.expects(:read).with("/path").never content.collect - content.instance_variable_get("@content").should_not be_nil + content.instance_variable_get("@content").should be_nil end it "should not attempt to retrieve its contents if the file is a directory" do content = Puppet::FileServing::Content.new("/path") result = "foo" File.stubs(:lstat).returns(stub("stat", :ftype => "directory")) File.expects(:read).with("/path").never content.collect content.instance_variable_get("@content").should be_nil end it "should have a method for setting its content" do content = Puppet::FileServing::Content.new("/path") content.should respond_to(:content=) end it "should make content available when set externally" do content = Puppet::FileServing::Content.new("/path") content.content = "foo/bar" content.content.should == "foo/bar" end it "should be able to create a content instance from raw file contents" do Puppet::FileServing::Content.should respond_to(:from_raw) end it "should create an instance with a fake file name and correct content when converting from raw" do instance = mock 'instance' Puppet::FileServing::Content.expects(:new).with("/this/is/a/fake/path").returns instance instance.expects(:content=).with "foo/bar" Puppet::FileServing::Content.from_raw("foo/bar").should equal(instance) end + + it "should return an opened File when converted to raw" do + content = Puppet::FileServing::Content.new("/path") + + File.expects(:new).with("/path","r").returns :file + + content.to_raw.should == :file + end end describe Puppet::FileServing::Content, "when returning the contents" do before do @path = "/my/path" @content = Puppet::FileServing::Content.new(@path, :links => :follow) end it "should fail if the file is a symlink and links are set to :manage" do @content.links = :manage File.expects(:lstat).with(@path).returns stub("stat", :ftype => "symlink") proc { @content.content }.should raise_error(ArgumentError) end it "should fail if a path is not set" do proc { @content.content() }.should raise_error(Errno::ENOENT) end it "should raise Errno::ENOENT if the file is absent" do @content.path = "/there/is/absolutely/no/chance/that/this/path/exists" proc { @content.content() }.should raise_error(Errno::ENOENT) end it "should return the contents of the path if the file exists" do File.expects(:stat).with(@path).returns stub("stat", :ftype => "file") File.expects(:read).with(@path).returns(:mycontent) @content.content.should == :mycontent end it "should cache the returned contents" do File.expects(:stat).with(@path).returns stub("stat", :ftype => "file") File.expects(:read).with(@path).returns(:mycontent) @content.content # The second run would throw a failure if the content weren't being cached. @content.content end end diff --git a/spec/unit/network/http/mongrel/rest.rb b/spec/unit/network/http/mongrel/rest.rb index fb7e7e57d..55b6172d3 100755 --- a/spec/unit/network/http/mongrel/rest.rb +++ b/spec/unit/network/http/mongrel/rest.rb @@ -1,232 +1,249 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../../spec_helper' require 'puppet/network/http' describe "Puppet::Network::HTTP::MongrelREST" do confine "Mongrel is not available" => Puppet.features.mongrel? before do require 'puppet/network/http/mongrel/rest' end it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::MongrelREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end describe "when initializing" do it "should call the Handler's initialization hook with its provided arguments as the server and handler" do Puppet::Network::HTTP::MongrelREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments") Puppet::Network::HTTP::MongrelREST.new(:server => "my", :handler => "arguments") end end describe "when receiving a request" do before do @params = {} @request = stub('mongrel http request', :params => @params) @head = stub('response head') @body = stub('response body', :write => true) @response = stub('mongrel http response') @response.stubs(:start).yields(@head, @body) @model_class = stub('indirected model class') @mongrel = stub('mongrel http server', :register => true) Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) @handler = Puppet::Network::HTTP::MongrelREST.new(:server => @mongrel, :handler => :foo) end describe "and using the HTTP Handler interface" do it "should return the HTTP_ACCEPT parameter as the accept header" do @params.expects(:[]).with("HTTP_ACCEPT").returns "myaccept" @handler.accept_header(@request).should == "myaccept" end it "should return the Content-Type parameter as the Content-Type header" do @params.expects(:[]).with("HTTP_CONTENT_TYPE").returns "mycontent" @handler.content_type_header(@request).should == "mycontent" end it "should use the REQUEST_METHOD as the http method" do @params.expects(:[]).with(Mongrel::Const::REQUEST_METHOD).returns "mymethod" @handler.http_method(@request).should == "mymethod" end it "should return the request path as the path" do @params.expects(:[]).with(Mongrel::Const::REQUEST_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 StringIO.new("mybody") @handler.body(@request).should == "mybody" end it "should set the response's content-type header when setting the content type" do @header = mock 'header' @response.expects(:header).returns @header @header.expects(:[]=).with('Content-Type', "mytype") @handler.set_content_type(@response, "mytype") end it "should set the status and write the body when setting the response for a successful request" do head = mock 'head' body = mock 'body' @response.expects(:start).with(200).yields(head, body) body.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 200) end + describe "when the result is a File" do + it "should use response send_file" do + head = mock 'head' + body = mock 'body' + stat = stub 'stat', :size => 100 + file = stub 'file', :stat => stat, :path => "/tmp/path" + file.stubs(:is_a?).with(File).returns(true) + + @response.expects(:start).with(200).yields(head, body) + @response.expects(:send_status).with(100) + @response.expects(:send_header) + @response.expects(:send_file).with("/tmp/path") + + @handler.set_response(@response, file, 200) + end + end + it "should set the status and reason and write the body when setting the response for a successful request" do head = mock 'head' body = mock 'body' @response.expects(:start).with(400, false, "mybody").yields(head, body) body.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 400) end end describe "and determining the request parameters" do before do @request.stubs(:params).returns({}) end it "should skip empty parameter values" do @request.expects(:params).returns('QUERY_STRING' => "&=") lambda { @handler.params(@request) }.should_not raise_error end it "should include the HTTP request parameters, with the keys as symbols" do @request.expects(:params).returns('QUERY_STRING' => 'foo=baz&bar=xyzzy') result = @handler.params(@request) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should CGI-decode the HTTP parameters" do encoding = CGI.escape("foo bar") @request.expects(:params).returns('QUERY_STRING' => "foo=#{encoding}") result = @handler.params(@request) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do @request.expects(:params).returns('QUERY_STRING' => 'foo=true') result = @handler.params(@request) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do @request.expects(:params).returns('QUERY_STRING' => 'foo=false') result = @handler.params(@request) result[:foo].should be_false end it "should convert integer arguments to Integers" do @request.expects(:params).returns('QUERY_STRING' => 'foo=15') result = @handler.params(@request) result[:foo].should == 15 end it "should convert floating point arguments to Floats" do @request.expects(:params).returns('QUERY_STRING' => 'foo=1.5') result = @handler.params(@request) result[:foo].should == 1.5 end it "should YAML-load and URI-decode values that are YAML-encoded" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.expects(:params).returns('QUERY_STRING' => "foo=#{escaping}") result = @handler.params(@request) result[:foo].should == %w{one two} end it "should not allow the client to set the node via the query string" do @request.stubs(:params).returns('QUERY_STRING' => "node=foo") @handler.params(@request)[:node].should be_nil end it "should not allow the client to set the IP address via the query string" do @request.stubs(:params).returns('QUERY_STRING' => "ip=foo") @handler.params(@request)[:ip].should be_nil end it "should pass the client's ip address to model find" do @request.stubs(:params).returns("REMOTE_ADDR" => "ipaddress") @handler.params(@request)[:ip].should == "ipaddress" end it "should pass the client's provided X-Forwared-For value as the ip" do @request.stubs(:params).returns("HTTP_X_FORWARDED_FOR" => "ipaddress") @handler.params(@request)[:ip].should == "ipaddress" end it "should pass the client's provided X-Forwared-For first value as the ip" do @request.stubs(:params).returns("HTTP_X_FORWARDED_FOR" => "ipproxy1,ipproxy2,ipaddress") @handler.params(@request)[:ip].should == "ipaddress" end it "should pass the client's provided X-Forwared-For value as the ip instead of the REMOTE_ADDR" do @request.stubs(:params).returns("REMOTE_ADDR" => "remote_addr") @request.stubs(:params).returns("HTTP_X_FORWARDED_FOR" => "ipaddress") @handler.params(@request)[:ip].should == "ipaddress" end it "should use the :ssl_client_header to determine the parameter when looking for the certificate" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => "/CN=host.domain.com") @handler.params(@request) end it "should retrieve the hostname by matching the certificate parameter" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => "/CN=host.domain.com") @handler.params(@request)[:node].should == "host.domain.com" end it "should use the :ssl_client_header to determine the parameter for checking whether the host certificate is valid" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.expects(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(@request) end it "should consider the host authenticated if the validity parameter contains 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(@request)[:authenticated].should be_true end it "should consider the host unauthenticated if the validity parameter does not contain 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "whatever", "certheader" => "/CN=host.domain.com") @handler.params(@request)[:authenticated].should be_false end it "should consider the host unauthenticated if no certificate information is present" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => nil, "certheader" => "SUCCESS") @handler.params(@request)[:authenticated].should be_false end it "should resolve the node name with an ip address look-up if no certificate is present" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => nil) @handler.expects(:resolve_node).returns("host.domain.com") @handler.params(@request)[:node].should == "host.domain.com" end end end end diff --git a/spec/unit/network/http/rack/rest.rb b/spec/unit/network/http/rack/rest.rb index e916712f3..75642f9f7 100755 --- a/spec/unit/network/http/rack/rest.rb +++ b/spec/unit/network/http/rack/rest.rb @@ -1,199 +1,249 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../../spec_helper' require 'puppet/network/http/rack' if Puppet.features.rack? require 'puppet/network/http/rack/rest' describe "Puppet::Network::HTTP::RackREST" do confine "Rack is not available" => Puppet.features.rack? it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::RackREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end describe "when initializing" do it "should call the Handler's initialization hook with its provided arguments" do Puppet::Network::HTTP::RackREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments") Puppet::Network::HTTP::RackREST.new(:server => "my", :handler => "arguments") end end describe "when serving a request" do before :all do @model_class = stub('indirected model class') Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) @handler = Puppet::Network::HTTP::RackREST.new(:handler => :foo) end before :each do @response = Rack::Response.new() end def mk_req(uri, opts = {}) env = Rack::MockRequest.env_for(uri, opts) Rack::Request.new(env) end describe "and using the HTTP Handler interface" do it "should return the HTTP_ACCEPT parameter as the accept header" do req = mk_req('/', 'HTTP_ACCEPT' => 'myaccept') @handler.accept_header(req).should == "myaccept" end it "should return the CONTENT_TYPE parameter as the content type header" do req = mk_req('/', 'CONTENT_TYPE' => 'mycontent') @handler.content_type_header(req).should == "mycontent" end it "should use the REQUEST_METHOD as the http method" do req = mk_req('/', :method => 'mymethod') @handler.http_method(req).should == "mymethod" end it "should return the request path as the path" do req = mk_req('/foo/bar') @handler.path(req).should == "/foo/bar" end it "should return the request body as the body" do req = mk_req('/foo/bar', :input => 'mybody') @handler.body(req).should == "mybody" end it "should set the response's content-type header when setting the content type" do @header = mock 'header' @response.expects(:header).returns @header @header.expects(:[]=).with('Content-Type', "mytype") @handler.set_content_type(@response, "mytype") end it "should set the status and write the body when setting the response for a request" do @response.expects(:status=).with(400) @response.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 400) end + + describe "when result is a File" do + before :each do + stat = stub 'stat', :size => 100 + @file = stub 'file', :stat => stat, :path => "/tmp/path" + @file.stubs(:is_a?).with(File).returns(true) + end + + it "should set the Content-Length header" do + @response.expects(:[]=).with("Content-Length", 100) + + @handler.set_response(@response, @file, 200) + end + + it "should return a RackFile adapter as body" do + @response.expects(:body=).with { |val| val.is_a?(Puppet::Network::HTTP::RackREST::RackFile) } + + @handler.set_response(@response, @file, 200) + end + end end describe "and determining the request parameters" do it "should include the HTTP request parameters, with the keys as symbols" do req = mk_req('/?foo=baz&bar=xyzzy') result = @handler.params(req) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should CGI-decode the HTTP parameters" do encoding = CGI.escape("foo bar") req = mk_req("/?foo=#{encoding}") result = @handler.params(req) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do req = mk_req("/?foo=true") result = @handler.params(req) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do req = mk_req("/?foo=false") result = @handler.params(req) result[:foo].should be_false end it "should convert integer arguments to Integers" do req = mk_req("/?foo=15") result = @handler.params(req) result[:foo].should == 15 end it "should convert floating point arguments to Floats" do req = mk_req("/?foo=1.5") result = @handler.params(req) result[:foo].should == 1.5 end it "should YAML-load and CGI-decode values that are YAML-encoded" do escaping = CGI.escape(YAML.dump(%w{one two})) req = mk_req("/?foo=#{escaping}") result = @handler.params(req) result[:foo].should == %w{one two} end it "should not allow the client to set the node via the query string" do req = mk_req("/?node=foo") @handler.params(req)[:node].should be_nil end it "should not allow the client to set the IP address via the query string" do req = mk_req("/?ip=foo") @handler.params(req)[:ip].should be_nil end it "should pass the client's ip address to model find" do req = mk_req("/", 'REMOTE_ADDR' => 'ipaddress') @handler.params(req)[:ip].should == "ipaddress" end it "should set 'authenticated' to false if no certificate is present" do req = mk_req('/') @handler.params(req)[:authenticated].should be_false end end describe "with pre-validated certificates" do it "should use the :ssl_client_header to determine the parameter when looking for the certificate" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" req = mk_req('/', "myheader" => "/CN=host.domain.com") @handler.params(req) end it "should retrieve the hostname by matching the certificate parameter" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" req = mk_req('/', "myheader" => "/CN=host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end it "should use the :ssl_client_header to determine the parameter for checking whether the host certificate is valid" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.expects(:value).with(:ssl_client_verify_header).returns "myheader" req = mk_req('/', "myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(req) end it "should consider the host authenticated if the validity parameter contains 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" req = mk_req('/', "myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(req)[:authenticated].should be_true end it "should consider the host unauthenticated if the validity parameter does not contain 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" req = mk_req('/', "myheader" => "whatever", "certheader" => "/CN=host.domain.com") @handler.params(req)[:authenticated].should be_false end it "should consider the host unauthenticated if no certificate information is present" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" req = mk_req('/', "myheader" => nil, "certheader" => "/CN=host.domain.com") @handler.params(req)[:authenticated].should be_false end it "should resolve the node name with an ip address look-up if no certificate is present" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" req = mk_req('/', "myheader" => nil) @handler.expects(:resolve_node).returns("host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end end end end + +describe Puppet::Network::HTTP::RackREST::RackFile do + before(:each) do + stat = stub 'stat', :size => 100 + @file = stub 'file', :stat => stat, :path => "/tmp/path" + @rackfile = Puppet::Network::HTTP::RackREST::RackFile.new(@file) + end + + it "should have an each method" do + @rackfile.should be_respond_to(:each) + end + + it "should yield file chunks by chunks" do + @file.expects(:read).times(3).with(8192).returns("1", "2", nil) + i = 1 + @rackfile.each do |chunk| + chunk.to_i.should == i + i += 1 + end + end + + it "should have a close method" do + @rackfile.should be_respond_to(:close) + end + + it "should delegate close to File close" do + @file.expects(:close) + @rackfile.close + end +end \ No newline at end of file diff --git a/spec/unit/network/http/webrick/rest.rb b/spec/unit/network/http/webrick/rest.rb index f5c563e0d..f726fd9a7 100755 --- a/spec/unit/network/http/webrick/rest.rb +++ b/spec/unit/network/http/webrick/rest.rb @@ -1,157 +1,180 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../../spec_helper' require 'puppet/network/http' 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 initializing" do it "should call the Handler's initialization hook with its provided arguments as the server and handler" do Puppet::Network::HTTP::WEBrickREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments") Puppet::Network::HTTP::WEBrickREST.new("my", "arguments") end 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 = stub('webrick http response', :status= => true, :body= => true) @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, :foo) 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 "when using the Handler interface" do it "should use the 'accept' request parameter as the Accept header" do @request.expects(:[]).with("accept").returns "foobar" @handler.accept_header(@request).should == "foobar" end it "should use the 'content-type' request header as the Content-Type header" do @request.expects(:[]).with("content-type").returns "foobar" @handler.content_type_header(@request).should == "foobar" end 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" @handler.set_response(@response, "mybody", 200) end + describe "when the result is a File" do + before(:each) do + stat = stub 'stat', :size => 100 + @file = stub 'file', :stat => stat, :path => "/tmp/path" + @file.stubs(:is_a?).with(File).returns(true) + end + + it "should serve it" do + @response.stubs(:[]=) + + @response.expects(:status=).with 200 + @response.expects(:body=).with @file + + @handler.set_response(@response, @file, 200) + end + + it "should set the Content-Length header" do + @response.expects(:[]=).with('content-length', 100) + + @handler.set_response(@response, @file, 200) + end + 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(:reason_phrase=).with "mybody" @handler.set_response(@response, "mybody", 400) end end describe "and determining the request parameters" do it "should include the HTTP request parameters, with the keys as symbols" do @request.stubs(:query).returns("foo" => "baz", "bar" => "xyzzy") result = @handler.params(@request) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should CGI-decode the HTTP parameters" do encoding = CGI.escape("foo bar") @request.expects(:query).returns('foo' => encoding) result = @handler.params(@request) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do @request.expects(:query).returns('foo' => "true") result = @handler.params(@request) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do @request.expects(:query).returns('foo' => "false") result = @handler.params(@request) result[:foo].should be_false end it "should YAML-load and CGI-decode values that are YAML-encoded" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.expects(:query).returns('foo' => escaping) 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.stubs(:query).returns("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.stubs(:query).returns("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 cert = stub 'cert', :subject => [%w{CN host.domain.com}] @request.stubs(:client_cert).returns cert @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 end end end