From: Christian Hofstaedtler <[email protected]>
Signed-off-by: Christian Hofstaedtler <[email protected]> --- lib/puppet/network/http/rack.rb | 21 ++++- lib/puppet/network/http/rack/xmlrpc.rb | 46 ++++++++ spec/unit/network/http/rack.rb | 29 +++++- spec/unit/network/http/rack/xmlrpc.rb | 182 ++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 lib/puppet/network/http/rack/xmlrpc.rb create mode 100755 spec/unit/network/http/rack/xmlrpc.rb diff --git a/lib/puppet/network/http/rack.rb b/lib/puppet/network/http/rack.rb index 55e50c4..cf6cdc8 100644 --- a/lib/puppet/network/http/rack.rb +++ b/lib/puppet/network/http/rack.rb @@ -2,6 +2,7 @@ require 'rack' require 'puppet/network/http' require 'puppet/network/http/rack/rest' +require 'puppet/network/http/rack/xmlrpc' # An rack application, for running the Puppet HTTP Server. class Puppet::Network::HTTP::Rack @@ -14,21 +15,37 @@ class Puppet::Network::HTTP::Rack @rest_http_handler = Puppet::Network::HTTP::RackREST.new() protocols.delete :rest + # Prepare the XMLRPC handler, for backward compatibility (if requested) + if args[:protocols].include?(:xmlrpc) + raise ArgumentError, "XMLRPC was requested, but no handlers were given" if !args.include?(:xmlrpc_handlers) + + @xmlrpc_http_handler = Puppet::Network::HTTP::RackXMLRPC.new(args[:xmlrpc_handlers]) + protocols.delete :xmlrpc + end + 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 + # * 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] + # if we shall serve XMLRPC, have /RPC2 go to the xmlrpc handler + if @xmlrpc_http_handler and request.path_info.start_with?('/RPC2') + handler = @xmlrpc_http_handler + else + # everything else is handled by the new REST handler + handler = @rest_http_handler + end + begin - @rest_http_handler.process(request, response) + handler.process(request, response) rescue => detail # Send a Status 500 Error on unhandled exceptions. response.status = 500 diff --git a/lib/puppet/network/http/rack/xmlrpc.rb b/lib/puppet/network/http/rack/xmlrpc.rb new file mode 100644 index 0000000..88a3c5a --- /dev/null +++ b/lib/puppet/network/http/rack/xmlrpc.rb @@ -0,0 +1,46 @@ +require 'puppet/network/http/rack/httphandler' +require 'puppet/network/xmlrpc/server' + +class Puppet::Network::HTTP::RackXMLRPC < Puppet::Network::HTTP::RackHttpHandler + def initialize(handlers) + @xmlrpc_server = Puppet::Network::XMLRPCServer.new + handlers.each do |name| + Puppet.debug " -> register xmlrpc namespace %s" % name + unless handler = Puppet::Network::Handler.handler(name) + raise ArgumentError, "Invalid XMLRPC handler %s" % name + end + @xmlrpc_server.add_handler(handler.interface, handler.new({})) + end + super() + end + + def process(request, response) + # errors are sent as text/plain + response['Content-Type'] = 'text/plain' + if not request.post? then + response.status = 405 + response.write 'Method Not Allowed' + return + end + if request.media_type() != "text/xml" then + response.status = 400 + response.write 'Bad Request' + return + end + + # get auth/certificate data + info = extract_client_info(request) + if not info[:node] + info[:node] = nil + end + info = Puppet::Network::ClientRequest.new(info[:node], info[:ip], info[:authenticated]) + + response_body = @xmlrpc_server.process(request.body, info) + + response.status = 200 + response['Content-Type'] = 'text/xml; charset=utf-8' + response.write response_body + end + +end + diff --git a/spec/unit/network/http/rack.rb b/spec/unit/network/http/rack.rb index 2a575e2..70fc8ad 100644 --- a/spec/unit/network/http/rack.rb +++ b/spec/unit/network/http/rack.rb @@ -10,7 +10,7 @@ describe "Puppet::Network::HTTP::Rack", "while initializing" do Proc.new { Puppet::Network::HTTP::Rack.new({}) }.should raise_error(ArgumentError) end - it "should only accept the REST protocol" do + it "should not accept imaginary protocols" do Proc.new { Puppet::Network::HTTP::Rack.new({:protocols => [:foo]}) }.should raise_error(ArgumentError) end @@ -23,6 +23,17 @@ describe "Puppet::Network::HTTP::Rack", "while initializing" do Puppet::Network::HTTP::Rack.new({:protocols => [:rest]}) end + describe "when serving XMLRPC" do + it "should require XMLRPC handlers" do + Proc.new { Puppet::Network::HTTP::Rack.new({:protocols => [:xmlrpc]}) }.should raise_error(ArgumentError) + end + + it "should create a RackXMLRPC instance" do + Puppet::Network::HTTP::RackXMLRPC.expects(:new) + Puppet::Network::HTTP::Rack.new({:protocols => [:xmlrpc], :xmlrpc_handlers => [:Status]}) + end + end + end describe "Puppet::Network::HTTP::Rack", "when called" do @@ -65,5 +76,21 @@ describe "Puppet::Network::HTTP::Rack", "when called" do Rack::Response.any_instance.expects(:finish).once @app.call(@env) # can't lint when finish is a stub end + + describe "when serving XMLRPC" do + before :all do + @app = Puppet::Network::HTTP::Rack.new({:protocols => [:rest, :xmlrpc], :xmlrpc_handlers => [:Status]}) + @linted = Rack::Lint.new(@app) + end + + before :each do + @env = Rack::MockRequest.env_for('/RPC2', :method => 'POST') + end + + it "should use RackXMLRPC to serve /RPC2 requests" do + Puppet::Network::HTTP::RackXMLRPC.any_instance.expects(:process).once + @linted.call(@env) + end + end end diff --git a/spec/unit/network/http/rack/xmlrpc.rb b/spec/unit/network/http/rack/xmlrpc.rb new file mode 100755 index 0000000..60a4cf7 --- /dev/null +++ b/spec/unit/network/http/rack/xmlrpc.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../../spec_helper' +require 'puppet/network/http/rack' +require 'puppet/network/http/rack/xmlrpc' + +describe "Puppet::Network::HTTP::RackXMLRPC" do + confine "Rack is not available" => Puppet.features.rack? + + describe "when initializing" do + it "should create an Puppet::Network::XMLRPCServer" do + Puppet::Network::XMLRPCServer.expects(:new).returns stub_everything + Puppet::Network::HTTP::RackXMLRPC.new([]) + end + + it "should create each handler" do + handler = stub_everything 'handler' + Puppet::Network::XMLRPCServer.any_instance.stubs(:add_handler) + Puppet::Network::Handler.expects(:handler).returns(handler).times(2) + Puppet::Network::HTTP::RackXMLRPC.new([:foo, :bar]) + end + + it "should add each handler to the XMLRPCserver" do + handler = stub_everything 'handler' + Puppet::Network::Handler.stubs(:handler).returns(handler) + Puppet::Network::XMLRPCServer.any_instance.expects(:add_handler).times(2) + Puppet::Network::HTTP::RackXMLRPC.new([:foo, :bar]) + end + end + + describe "when serving a request" do + + before :each do + foo_handler = stub_everything 'foo_handler' + Puppet::Network::Handler.stubs(:handler).with(:foo).returns foo_handler + Puppet::Network::XMLRPCServer.any_instance.stubs(:add_handler) + Puppet::Network::XMLRPCServer.any_instance.stubs(:process).returns('<xml/>') + @handler = Puppet::Network::HTTP::RackXMLRPC.new([:foo]) + end + + before :each do + @response = Rack::Response.new() + end + + def mk_req(opts = {}) + opts[:method] = 'POST' if !opts[:method] + opts['CONTENT_TYPE'] = 'text/xml; foo=bar' if !opts['CONTENT_TYPE'] + env = Rack::MockRequest.env_for('/RPC2', opts) + Rack::Request.new(env) + end + + it "should reject non-POST requests" do + req = mk_req :method => 'PUT' + @handler.process(req, @response) + @response.status.should == 405 + end + + it "should reject non text/xml requests" do + req = mk_req 'CONTENT_TYPE' => 'yadda/plain' + end + + it "should create a ClientRequest" do + cr = Puppet::Network::ClientRequest.new(nil, '127.0.0.1', false) + Puppet::Network::ClientRequest.expects(:new).returns cr + req = mk_req + @handler.process(req, @response) + end + + it "should let xmlrpcserver process the request" do + Puppet::Network::XMLRPCServer.any_instance.expects(:process).returns('yay') + req = mk_req + @handler.process(req, @response) + end + + it "should report the response as OK" do + req = mk_req + @handler.process(req, @response) + @response.status.should == 200 + end + + it "should report the response with the correct content type" do + req = mk_req + @handler.process(req, @response) + @response['Content-Type'].should == 'text/xml; charset=utf-8' + end + + it "should set 'authenticated' to false if no certificate is present" do + req = mk_req + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| authenticated == false } + @handler.process(req, @response) + end + + it "should use the client's ip address" do + req = mk_req 'REMOTE_ADDR' => 'ipaddress' + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| ip == 'ipaddress' } + @handler.process(req, @response) + 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.process(req, @response) + 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" + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| node == "host.domain.com" } + req = mk_req "myheader" => "/CN=host.domain.com" + @handler.process(req, @response) + 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.process(req, @response) + 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" + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| authenticated == true } + req = mk_req "myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com" + @handler.process(req, @response) + 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" + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| authenticated == false } + req = mk_req "myheader" => "whatever", "certheader" => "/CN=host.domain.com" + @handler.process(req, @response) + 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" + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| authenticated == false } + req = mk_req "myheader" => nil, "certheader" => "/CN=host.domain.com" + @handler.process(req, @response) + end + + it "should not have a node name if no certificate information is present" do + Puppet.settings.stubs(:value).returns "eh" + Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| node == nil } + req = mk_req "myheader" => nil + @handler.process(req, @response) + 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) + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| authenticated == true } + req = mk_req 'SSL_CLIENT_CERT' => cert + @handler.process(req, @response) + 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) + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| node == "host.domain.com" } + req = mk_req 'SSL_CLIENT_CERT' => cert + @handler.process(req, @response) + end + + it "should not have a node name if no certificate is present" do + req = mk_req 'SSL_CLIENT_CERT' => nil + Puppet::Network::ClientRequest.expects(:new).with() { |node,ip,authenticated| node == nil } + @handler.process(req, @response) + 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 -~----------~----~----~----~------~----~------~--~---
