* Luke Kanies <[email protected]> [090420 05:41]:
> 
> On Apr 19, 2009, at 1:51 PM, Christian Hofstaedtler wrote:
> 
> >
> > 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?
> 
> Shouldn't this fail if the 'rack' feature is missing?

Just requiring puppet/network/http/rack will fail in this case. I
could change this though and/or fail explicitly in initialize().

> >
> > +    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'
> 
> It'd be great if this at least gave the error, too.

I'll look into this.
BTW, I can't seem to successfully test the rescue case with
rspec/mocha: the Exception doesn't get caught while testing.

> >
> > +            # 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']
> 
> Should this environment variable be extracted into a setting?
> 
> I would have figured we'd already have such a setting, but I guess not.

SSL_CLIENT_CERT doesn't seem to be configurable right now; but see
below.

> >
> > +            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
> 
> This actually won't work.  First, this is not the cheapest process,  
> such that you probably don't want to do it on every single connection,  
> but most importantly, we can't promise that the server will have every  
> certificate.  SSL should handle this for us, but how Rack does it, I  
> don't know.   I know when Apache is used with Mongrel, we have it set  
> a couple of variables, one with the client's DN and the other with the  
> authentication status.  I see you're familiar with the header  
> settings; there's got to be something similar here.

Rack itself doesn't (mostly) care about SSL; it just passes through what 
the hosting webserver gives us.

If we won't support hosting Rack on top of WEBrick, we can really
just throw away this case (SSL_CLIENT_CERT). Apache with Passenger
and Mongrel are covered by the code below (by comparison of
Puppet[:ssl_client_header]), as you precisley describe.

> >
> > +        # 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?
> 
> Nice catch.
> 
> >
> > +    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?
> 
> If you nest all of your contexts, then a single confine in the outer  
> context will suffice.

Done.

> > [...]

  Christian


--~--~---------~--~----~------------~-------~--~----~
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