summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/feature/base.rb3
-rw-r--r--lib/puppet/network/http/rack.rb45
-rw-r--r--lib/puppet/network/http/rack/httphandler.rb16
-rw-r--r--lib/puppet/network/http/rack/rest.rb74
-rwxr-xr-xspec/unit/network/http/rack.rb71
-rwxr-xr-xspec/unit/network/http/rack/rest.rb194
6 files changed, 403 insertions, 0 deletions
diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb
index c3fb9a2f3..7c0f241c1 100644
--- a/lib/puppet/feature/base.rb
+++ b/lib/puppet/feature/base.rb
@@ -28,3 +28,6 @@ Puppet.features.add(:augeas, :libs => ["augeas"])
# We have RRD available
Puppet.features.add(:rrd, :libs => ["RRDtool"])
+
+# We have rack available, an HTTP Application Stack
+Puppet.features.add(:rack, :libs => ["rack"])
diff --git a/lib/puppet/network/http/rack.rb b/lib/puppet/network/http/rack.rb
new file mode 100644
index 000000000..58f49416b
--- /dev/null
+++ b/lib/puppet/network/http/rack.rb
@@ -0,0 +1,45 @@
+
+require 'rack'
+require 'puppet/network/http'
+require 'puppet/network/http/rack/rest'
+
+# An rack application, for running the Puppet HTTP Server.
+class Puppet::Network::HTTP::Rack
+
+ def initialize(args)
+ raise ArgumentError, ":protocols must be specified." if !args[:protocols] or args[:protocols].empty?
+ protocols = args[:protocols]
+
+ # Always prepare a REST handler
+ @rest_http_handler = Puppet::Network::HTTP::RackREST.new()
+ protocols.delete :rest
+
+ raise ArgumentError, "there were unknown :protocols specified." if !protocols.empty?
+ end
+
+ # The real rack application (which needs to respond to call).
+ # The work we need to do, roughly is:
+ # * Read request (from env) and prepare a response
+ # * Route the request to the correct handler
+ # * Return the response (in rack-format) to our caller.
+ def call(env)
+ request = Rack::Request.new(env)
+ response = Rack::Response.new()
+ Puppet.debug 'Handling request: %s %s' % [request.request_method, request.fullpath]
+
+ begin
+ @rest_http_handler.process(request, response)
+ rescue => detail
+ # Send a Status 500 Error on unhandled exceptions.
+ response.status = 500
+ response['Content-Type'] = 'text/plain'
+ response.write 'Internal Server Error: "%s"' % detail.message
+ # log what happened
+ Puppet.err "Puppet Server (Rack): Internal Server Error: Unhandled Exception: \"%s\"" % detail.message
+ Puppet.err "Backtrace:"
+ detail.backtrace.each { |line| Puppet.err " > %s" % line }
+ end
+ response.finish()
+ end
+end
+
diff --git a/lib/puppet/network/http/rack/httphandler.rb b/lib/puppet/network/http/rack/httphandler.rb
new file mode 100644
index 000000000..e14206850
--- /dev/null
+++ b/lib/puppet/network/http/rack/httphandler.rb
@@ -0,0 +1,16 @@
+require 'openssl'
+require 'puppet/ssl/certificate'
+
+class Puppet::Network::HTTP::RackHttpHandler
+
+ def initialize()
+ end
+
+ # do something useful with request (a Rack::Request) and use
+ # response to fill your Rack::Response
+ def process(request, response)
+ raise NotImplementedError, "Your RackHttpHandler subclass is supposed to override service(request)"
+ end
+
+end
+
diff --git a/lib/puppet/network/http/rack/rest.rb b/lib/puppet/network/http/rack/rest.rb
new file mode 100644
index 000000000..e98bffc1e
--- /dev/null
+++ b/lib/puppet/network/http/rack/rest.rb
@@ -0,0 +1,74 @@
+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
+
+ def initialize(args={})
+ super()
+ initialize_for_puppet(args)
+ end
+
+ def set_content_type(response, format)
+ response[ContentType] = format
+ end
+
+ # produce the body of the response
+ def set_response(response, result, status = 200)
+ response.status = status
+ response.write result
+ end
+
+ # Retrieve the accept header from the http request.
+ def accept_header(request)
+ request.env[HEADER_ACCEPT]
+ 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/spec/unit/network/http/rack.rb b/spec/unit/network/http/rack.rb
new file mode 100755
index 000000000..3d5e4fe63
--- /dev/null
+++ b/spec/unit/network/http/rack.rb
@@ -0,0 +1,71 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+require 'puppet/network/http/rack'
+
+describe "Puppet::Network::HTTP::Rack" do
+ confine "Rack is not available" => Puppet.features.rack?
+
+ describe "while initializing" do
+
+ it "should require a protocol specification" do
+ Proc.new { Puppet::Network::HTTP::Rack.new({}) }.should raise_error(ArgumentError)
+ end
+
+ it "should not accept imaginary protocols" do
+ Proc.new { Puppet::Network::HTTP::Rack.new({:protocols => [:foo]}) }.should raise_error(ArgumentError)
+ end
+
+ it "should accept the REST protocol" do
+ Proc.new { Puppet::Network::HTTP::Rack.new({:protocols => [:rest]}) }.should_not raise_error(ArgumentError)
+ end
+
+ it "should create a RackREST instance" do
+ Puppet::Network::HTTP::RackREST.expects(:new)
+ Puppet::Network::HTTP::Rack.new({:protocols => [:rest]})
+ end
+
+ end
+
+ describe "when called" do
+
+ before :all do
+ @app = Puppet::Network::HTTP::Rack.new({:protocols => [:rest]})
+ # let's use Rack::Lint to verify that we're OK with the rack specification
+ @linted = Rack::Lint.new(@app)
+ end
+
+ before :each do
+ @env = Rack::MockRequest.env_for('/')
+ end
+
+ it "should create a Request object" do
+ request = Rack::Request.new(@env)
+ Rack::Request.expects(:new).returns request
+ @linted.call(@env)
+ end
+
+ it "should create a Response object" do
+ Rack::Response.expects(:new).returns stub_everything
+ @app.call(@env) # can't lint when Rack::Response is a stub
+ end
+
+ it "should let RackREST process the request" do
+ Puppet::Network::HTTP::RackREST.any_instance.expects(:process).once
+ @linted.call(@env)
+ end
+
+ it "should catch unhandled exceptions from RackREST" do
+ Puppet::Network::HTTP::RackREST.any_instance.expects(:process).raises(ArgumentError, 'test error')
+ Proc.new { @linted.call(@env) }.should_not raise_error
+ end
+
+ it "should finish() the Response" do
+ Rack::Response.any_instance.expects(:finish).once
+ @app.call(@env) # can't lint when finish is a stub
+ end
+
+ end
+
+end
+
diff --git a/spec/unit/network/http/rack/rest.rb b/spec/unit/network/http/rack/rest.rb
new file mode 100755
index 000000000..873483782
--- /dev/null
+++ b/spec/unit/network/http/rack/rest.rb
@@ -0,0 +1,194 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+require 'puppet/network/http/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 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
+ 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 URI-decode the HTTP parameters" do
+ encoding = URI.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 URI-decode values that are YAML-encoded" do
+ escaping = URI.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