On Apr 20, 2009, at 2:54 PM, Christian Hofstaedtler wrote:
>
> * 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().
As long as the error the user gets is helpful, I don't really care;
but 'Could not find constant' or whatever is a pretty bad error,
especially if Rails is installs because it monkey-patches the constant
lookup system so we get even more useless exceptions.
>
>>>
>>> + 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.
+ 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
Try raising 'RuntimeError' or ArgumentError; I think 'rescue' doesn't
catch the Exception class (or things like LoadError) by default.
>>> + # 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.
Okay, that sounds best.
>>> + # 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
>
>
> >
--
In science, 'fact' can only mean 'confirmed to such a degree that it
would be perverse to withhold provisional assent.' I suppose that
apples might start to rise tomorrow, but the possibility does not
merit equal time in physics classrooms. -- Stephen Jay Gould
---------------------------------------------------------------------
Luke Kanies | http://reductivelabs.com | http://madstop.com
--~--~---------~--~----~------------~-------~--~----~
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
-~----------~----~----~----~------~----~------~--~---