From: Christian Hofstaedtler <[email protected]>

This lays the ground: a wrapper for the REST handler, and an application
confirming to the Rack standard.

Signed-off-by: Christian Hofstaedtler <[email protected]>
---
 lib/puppet/feature/base.rb           |    3 +
 lib/puppet/network/http/rack.rb      |   45 +++++++
 lib/puppet/network/http/rack/rest.rb |   88 ++++++++++++++
 spec/unit/network/http/rack.rb       |   69 +++++++++++
 spec/unit/network/http/rack/rest.rb  |  216 ++++++++++++++++++++++++++++++++++
 5 files changed, 421 insertions(+), 0 deletions(-)
 create mode 100644 lib/puppet/network/http/rack.rb
 create mode 100644 lib/puppet/network/http/rack/rest.rb
 create mode 100644 spec/unit/network/http/rack.rb
 create mode 100755 spec/unit/network/http/rack/rest.rb

diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb
index c3fb9a2..7c0f241 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 0000000..55e50c4
--- /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
+    # * 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'
+            # 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/rest.rb 
b/lib/puppet/network/http/rack/rest.rb
new file mode 100644
index 0000000..5679c41
--- /dev/null
+++ b/lib/puppet/network/http/rack/rest.rb
@@ -0,0 +1,88 @@
+require 'puppet/network/http/handler'
+
+class Puppet::Network::HTTP::RackREST
+
+    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?
+    def path(request)
+        request.fullpath
+    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)
+        ip = request.ip
+        valid = false
+        client = nil
+
+        # if we find an SSL cert in the headers, use it to get a hostname
+        # (for WEBrick, or Apache with ExportCertData)
+        if request.env['SSL_CLIENT_CERT']
+            cert = 
OpenSSL::X509::Certificate.new(request.env['SSL_CLIENT_CERT'])
+            nameary = cert.subject.to_a.find { |ary|
+                ary[0] == "CN"
+            }
+            if nameary
+                client = nameary[1]
+                # XXX: certificate validation works by finding the supposed
+                # cert the client should be using, and comparing that to what
+                # got sent. this *should* be fine, but maybe it's not?
+                valid = (Puppet::SSL::Certificate.find(client).to_text == 
cert.to_text)
+            end
+
+        # now try with :ssl_client_header, which defaults should work for
+        # Apache with StdEnvVars.
+        elsif dn = request.env[Puppet[:ssl_client_header]] and dn_matchdata = 
dn.match(/^.*?CN\s*=\s*(.*)/)
+            client = dn_matchdata[1].to_str
+            valid = (request.env[Puppet[:ssl_client_verify_header]] == 
'SUCCESS')
+        end
+
+        result = {:ip => ip, :authenticated => valid}
+        if client
+          result[:node] = client
+        end
+        result
+    end
+end
diff --git a/spec/unit/network/http/rack.rb b/spec/unit/network/http/rack.rb
new file mode 100644
index 0000000..2a575e2
--- /dev/null
+++ b/spec/unit/network/http/rack.rb
@@ -0,0 +1,69 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+require 'puppet/network/http/rack'
+
+describe "Puppet::Network::HTTP::Rack", "while initializing" do
+    confine "Rack is not available" => Puppet.features.rack?
+
+    it "should require a protocol specification" do
+        Proc.new { Puppet::Network::HTTP::Rack.new({}) }.should 
raise_error(ArgumentError)
+    end
+
+    it "should only accept the REST protocol" 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 "Puppet::Network::HTTP::Rack", "when called" do
+    confine "Rack is not available" => Puppet.features.rack?
+
+    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
+        pending("check why the exception doesn't get rescue'd properly") do
+            
Puppet::Network::HTTP::RackREST.any_instance.expects(:process).raises(Exception,
 'test error')
+            Proc.new { @linted.call(@env) }.should_not raise_error
+        end
+    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
+
diff --git a/spec/unit/network/http/rack/rest.rb 
b/spec/unit/network/http/rack/rest.rb
new file mode 100755
index 0000000..3179de8
--- /dev/null
+++ b/spec/unit/network/http/rack/rest.rb
@@ -0,0 +1,216 @@
+#!/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 not pass a node name to model method if no certificate 
information 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.params(req).should_not be_include(:node)
+            end
+        end
+
+        describe "with in-header certificates" do
+            it "should set 'authenticated' to true if a certificate is 
present" do
+                cert = stub 'cert', :subject => [%w{CN host.domain.com}], 
:to_text => 'yay'
+                OpenSSL::X509::Certificate.expects(:new).returns(cert)
+                
Puppet::SSL::Certificate.expects(:find).with('host.domain.com').returns(cert)
+                req = mk_req('/', 'SSL_CLIENT_CERT' => cert)
+                @handler.params(req)[:authenticated].should be_true
+            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}], 
:to_text => 'yay'
+                OpenSSL::X509::Certificate.expects(:new).returns(cert)
+                
Puppet::SSL::Certificate.expects(:find).with('host.domain.com').returns(cert)
+                req = mk_req('/', 'SSL_CLIENT_CERT' => cert)
+                @handler.params(req)[:node].should == "host.domain.com"
+            end
+
+            it "should not pass a node name to model method if no certificate 
is present" do
+                req = mk_req('/', 'SSL_CLIENT_CERT' => nil)
+                @handler.params(req).should_not be_include(:node)
+            end
+        end
+    end
+end
-- 
1.5.6.5


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to 
[email protected]
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to