From: <jh...@us...> - 2009-09-11 16:59:30
|
Revision: 87 http://etch.svn.sourceforge.net/etch/?rev=87&view=rev Author: jheiss Date: 2009-09-11 16:59:21 +0000 (Fri, 11 Sep 2009) Log Message: ----------- Added authentication support. The client will sign messages using its ssh host key. Add support for local requests. Update the method used to eliminate the OpenSSL "using default DH parameters" warning. http://techbits.blogspot.com/2009/08/ive-been-using-workaround-shown-at.html Add timeout to output capturing so that ill-behaved daemons don't cause etch to hang around forever. Add unit tests for output capturing. Note that the output capturing timeout test is commented out as the timeout is rather long, thus causing the test to take a long time to run. Not quite sure the best way to handle that. Move Etch::Client.initialize arguments to a hash so that the method doesn't take 10 or whatever arguments. Set etch version as a constant in the library (Etch::Client::VERSION). Add a command line option to etch to display the version. Modify the make targets to insert the version into the appropriate place rather than having the version hard-coded in a bunch of files. Add redhatprep, debianprep and solarisprep targets which install the packages necessary to extract the version from the library file. Add code to require rubygems if requiring facter fails. Change /usr/lib/ruby/site_ruby to /usr/local/lib/ruby/site_ruby for the Debian package. That seems to be the correct location on Debian systems. Modified Paths: -------------- trunk/client/Makefile trunk/client/control trunk/client/etch trunk/client/etch-client.spec trunk/client/etch.rb trunk/client/pkginfo Added Paths: ----------- trunk/client/dhparams Modified: trunk/client/Makefile =================================================================== --- trunk/client/Makefile 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/Makefile 2009-09-11 16:59:21 UTC (rev 87) @@ -1,13 +1,15 @@ -# When updating the version number you currently need to update -# etch-client.spec, control and pkginfo as well -VER=1.1 +BUILDROOT=/var/tmp/etch-client-buildroot +# Grab the current version from the library +VER=$(shell ruby -e "$$:.unshift('.'); require 'etch'; puts Etch::Client::VERSION") all: -redhat: rpmbuild-redhat rpm -rpmbuild-redhat: - rpm --quiet -q rpm-build || yum -y install rpm-build -BUILDROOT=/var/tmp/etch-client-buildroot +redhat: redhatprep rpm +redhatprep: + # Install everything needed for the command which sets VER above + rpm --quiet -q ruby || sudo yum install ruby + # And the package which contains the rpmbuild command + rpm --quiet -q rpm-build || sudo yum install rpm-build TMPSPEC = etch-client-temp.spec rpm: etch-client.spec # @@ -22,6 +24,7 @@ chmod 444 $(BUILDROOT)/usr/lib/ruby/site_ruby/1.8/etch.rb mkdir -p $(BUILDROOT)/etc/etch cp -p ca.pem $(BUILDROOT)/etc/etch + cp -p dhparams $(BUILDROOT)/etc/etch # Cron job mkdir -p $(BUILDROOT)/etc/cron.d cp etch_cron $(BUILDROOT)/etc/cron.d/etch @@ -30,26 +33,38 @@ # # Now build the package # - rpmbuild -bb --buildroot $(BUILDROOT) etch-client.spec + sed 's/VER/$(VER)/' etch-client.spec > etch-client.spec_withversion + rpmbuild -bb --buildroot $(BUILDROOT) etch-client.spec_withversion rm -rf $(BUILDROOT) -debian: control - rm -rf debtmp - mkdir -p debtmp/DEBIAN - grep -v '^#' control > debtmp/DEBIAN/control - mkdir -p debtmp/usr/sbin - cp -p etch debtmp/usr/sbin - chmod 555 debtmp/usr/sbin/etch - mkdir -p debtmp/usr/lib/ruby/site_ruby/1.8 - cp -p etch.rb debtmp/usr/lib/ruby/site_ruby/1.8 - chmod 444 debtmp/usr/lib/ruby/site_ruby/1.8 - mkdir -p debtmp/etc/etch - cp -p ca.pem debtmp/etc/etch - sudo chown -R 0:0 debtmp - dpkg --build debtmp etch-client-$(VER).deb - rm -rf debtmp +debian: debianprep deb +debianprep: + # Install everything needed for the command which sets VER above + sudo apt-get --no-upgrade --quiet install ruby libopenssl-ruby rubygems facter +deb: control + rm -rf $(BUILDROOT) + mkdir -p $(BUILDROOT)/DEBIAN + grep -v '^#' control | sed 's/VER/$(VER)/' > $(BUILDROOT)/DEBIAN/control + mkdir -p $(BUILDROOT)/usr/sbin + cp -p etch $(BUILDROOT)/usr/sbin + chmod 555 $(BUILDROOT)/usr/sbin/etch + mkdir -p $(BUILDROOT)/usr/local/lib/site_ruby/1.8 + cp -p etch.rb $(BUILDROOT)/usr/local/lib/site_ruby/1.8 + chmod 444 $(BUILDROOT)/usr/local/lib/site_ruby/1.8/etch.rb + mkdir -p $(BUILDROOT)/etc/etch + cp -p ca.pem $(BUILDROOT)/etc/etch + cp -p dhparams $(BUILDROOT)/etc/etch + sudo chown -R 0:0 $(BUILDROOT) + dpkg --build $(BUILDROOT) etch-client-$(VER).deb + rm -rf $(BUILDROOT) -solaris: pkginfo depend +solaris: solarisprep sysvpkg sysvpkg-sparc +solarisprep: + # Install everything needed for the command which sets VER above + pkginfo -q CSWruby || sudo pkg-get -i ruby + pkginfo -q CSWrubygems || sudo pkg-get -i rubygems + pkginfo -q CSWfacter || sudo pkg-get -i facter +sysvpkg: pkginfo depend # # Create package file structure in build root # @@ -65,27 +80,33 @@ chmod 444 $(BUILDROOT)/opt/csw/lib/ruby/site_ruby/1.8/etch.rb mkdir -p $(BUILDROOT)/etc/etch cp -p ca.pem $(BUILDROOT)/etc/etch + cp -p dhparams $(BUILDROOT)/etc/etch # Cron job for registration cat etch_cron_wrapper | sed 's,/usr/bin/perl,/opt/csw/bin/perl,' > $(BUILDROOT)/usr/sbin/etch_cron_wrapper chmod 555 $(BUILDROOT)/usr/sbin/etch_cron_wrapper # # Now build the package # - echo "i pkginfo=./pkginfo" > prototype - echo "i depend=./depend" >> prototype - echo "i postinstall=./postinstall" >> prototype - echo "i postremove=./postremove" >> prototype + rm -rf solbuild + mkdir solbuild + sed 's/%VER%/$(VER)/' pkginfo > solbuild/pkginfo + echo "i pkginfo=./pkginfo" > solbuild/prototype + cp depend solbuild/depend + echo "i depend=./depend" >> solbuild/prototype + cp postinstall solbuild/postinstall + echo "i postinstall=./postinstall" >> solbuild/prototype + cp postremove solbuild/postremove + echo "i postremove=./postremove" >> solbuild/prototype # The tail +2 removes the first line, which is the base directory # and doesn't need to be included in the package. # The first sed just cleans up the directory names # The second sed tell pkgadd to not force our permissions on directories # The $$ in that sed escapes the $ from make - find $(BUILDROOT) | pkgproto | tail +2 | sed "s,$(BUILDROOT),," | sed '/^d/s/[^ ]* [^ ]* [^ ]*$$/? ? ?/' >> prototype - pkgmk -r $(BUILDROOT) -d $(PWD) - pkgtrans . YPCetch-$(VER).pkg YPCetch - rm -rf YPCetch + find $(BUILDROOT) | pkgproto | tail +2 | sed "s,$(BUILDROOT),," | sed '/^d/s/[^ ]* [^ ]* [^ ]*$$/? ? ?/' >> solbuild/prototype + cd solbuild && pkgmk -r $(BUILDROOT) -d $(PWD)/solbuild + pkgtrans solbuild ../YPCetch-$(VER).pkg YPCetch + rm -rf solbuild rm -rf $(BUILDROOT) - rm -f prototype # On Sparc systems we're having problems with the CSW/Blastwave ruby # core dumping when running etch. The Sunfreeware ruby seems to work. @@ -94,7 +115,7 @@ # our library file (etch.rb) into /opt/csw. We modify etch to use # the Sunfreeware ruby in /usr/local/bin, but then tell it to also look # in the /opt/csw directory for libraries. -solaris-sparc: pkginfo depend +sysvpkg-sparc: pkginfo depend # # Create package file structure in build root # @@ -114,25 +135,31 @@ chmod 444 $(BUILDROOT)/opt/csw/lib/ruby/site_ruby/1.8/etch.rb mkdir -p $(BUILDROOT)/etc/etch cp -p ca.pem $(BUILDROOT)/etc/etch + cp -p dhparams $(BUILDROOT)/etc/etch # Cron job for registration cat etch_cron_wrapper | sed 's,/usr/bin/perl,/opt/csw/bin/perl,' > $(BUILDROOT)/usr/sbin/etch_cron_wrapper chmod 555 $(BUILDROOT)/usr/sbin/etch_cron_wrapper # # Now build the package # - echo "i pkginfo=./pkginfo" > prototype - echo "i depend=./depend" >> prototype - echo "i postinstall=./postinstall" >> prototype - echo "i postremove=./postremove" >> prototype + rm -rf solbuild + mkdir solbuild + sed 's/%VER%/$(VER)/' pkginfo > solbuild/pkginfo + echo "i pkginfo=./pkginfo" > solbuild/prototype + cp depend solbuild/depend + echo "i depend=./depend" >> solbuild/prototype + cp postinstall solbuild/postinstall + echo "i postinstall=./postinstall" >> solbuild/prototype + cp postremove solbuild/postremove + echo "i postremove=./postremove" >> solbuild/prototype # The tail +2 removes the first line, which is the base directory # and doesn't need to be included in the package. # The first sed just cleans up the directory names # The second sed tell pkgadd to not force our permissions on directories # The $$ in that sed escapes the $ from make - find $(BUILDROOT) | pkgproto | tail +2 | sed "s,$(BUILDROOT),," | sed '/^d/s/[^ ]* [^ ]* [^ ]*$$/? ? ?/' >> prototype - pkgmk -r $(BUILDROOT) -d $(PWD) - pkgtrans . YPCetch-$(VER)-sparc.pkg YPCetch - rm -rf YPCetch + find $(BUILDROOT) | pkgproto | tail +2 | sed "s,$(BUILDROOT),," | sed '/^d/s/[^ ]* [^ ]* [^ ]*$$/? ? ?/' >> solbuild/prototype + cd solbuild && pkgmk -r $(BUILDROOT) -d $(PWD)/solbuild + pkgtrans solbuild ../YPCetch-$(VER)-sparc.pkg YPCetch + rm -rf solbuild rm -rf $(BUILDROOT) - rm -f prototype Modified: trunk/client/control =================================================================== --- trunk/client/control 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/control 2009-09-11 16:59:21 UTC (rev 87) @@ -1,5 +1,5 @@ Package: etch-client -Version: 1.1-1 +Version: VER-1 Maintainer: etc...@li... Architecture: all Depends: ruby facter rcs Added: trunk/client/dhparams =================================================================== --- trunk/client/dhparams (rev 0) +++ trunk/client/dhparams 2009-09-11 16:59:21 UTC (rev 87) @@ -0,0 +1,9 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA3HySq1WdL67BCSRCJCZYMUIojAWAsvK63D3cOGk0wI9UeM/yeVhz +jTswvHOVPZFKIBg1Aeo2eAEdPryDnmjVTgvLbuWkCPouQhBCVsQ1El9ZcXPix1rC +tYsg4Kll1jgnwFoHf4xvjPnD/SqsASAiDxYlh4CFVyT1gLgSiUU0rIdudgO3agI5 +NgiyGOKwyHmNOOQSKA62M/JnoxcBDC7Nou3lqtHpR5yWsUz+csyk+hXZeUba97bm +M8OB0PmfK4Vo6JpdO+yc8hjeYBoMsH7g/l3Gm1JqUxxctcY/OuJ+2nkXwsD66E3D +yZCoiVd3u4OqAxNO/GG0iUmskjIvokMhUwIBAg== +-----END DH PARAMETERS----- + Modified: trunk/client/etch =================================================================== --- trunk/client/etch 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/etch 2009-09-11 16:59:21 UTC (rev 87) @@ -13,25 +13,16 @@ # Parse the command line options # -$generateall = nil -$dryrun = nil -$interactive = nil -$fullfile = nil -$filenameonly = nil -$disableforce = nil -$lockforce = nil -$debug = nil -$server = nil -$tag = nil -$varbase = nil +options = {} +@generateall = nil opts = OptionParser.new opts.banner = 'Usage: etch [options] [/path/to/config/file]' opts.on('--generate-all', 'Can be used instead of giving a specific file to generate.') do |opt| - $generateall = opt + @generateall = opt end opts.on('--dry-run', '-n', 'Prints contents of generated files instead of writing them out to disk.') do |opt| - $dryrun = opt + options[:dryrun] = opt end opts.on('--damp-run', "Perform a dry run but run 'setup' entries for files. Normally all setup/pre/post entries are ignored for a dry run. However, files with setup entries will generally fail to build if the setup entry hasn't been run.") do |opt| # Rather than sprinkle checks of two different variables throught the code, if @@ -39,35 +30,42 @@ # value. Then we can just check for that specific value at the one place where # the two modes differ: enabling or disabling the execution of 'setup' # entries. - $dryrun = 'damp' + options[:dryrun] = 'damp' end opts.on('--interactive', 'Causes etch to pause before making each change and prompt the user for confirmation.') do |opt| - $interactive = opt + options[:interactive] = opt end opts.on('--full-file', 'Normally etch will print a diff to show what changes it will make to a file. This will cause etch to display the full new file contents instead.') do |opt| - $fullfile = opt + options[:fullfile] = opt end opts.on('--filename-only', 'Similar to the previous option, but in the opposite direction. Etch will only display the name of file to be changed.') do |opt| - $filenameonly = opt + options[:filenameonly] = opt end opts.on('--disable-force', 'Ignore the disable_etch file. Use with caution.') do |opt| - $disableforce = opt + options[:disableforce] = opt end opts.on('--lock-force', 'Force the removal of any existing lockfiles. Normally only lockfile over 2 hours old are removed.') do |opt| - $lockforce = opt + options[:lockforce] = opt end opts.on('--debug', 'Print lots of messages about what etch is doing') do |opt| - $debug = opt + options[:debug] = opt end opts.on('--server SERVER', 'Point etch to an alternate server') do |opt| - $server = opt + options[:server] = opt end opts.on('--tag TAG', 'Request a specific repository tag from the server') do |opt| - $tag = opt + options[:tag] = opt end opts.on('--test-base TESTDIR', 'Use an alternate local working directory (for use by test suite only)') do |opt| - $varbase = opt + options[:varbase] = opt end +opts.on('--key PRIVATE_KEY', 'Use this private key for signing messages that are sent to the server') do |opt| + options[:key] = opt +end +opts.on('--version', 'Show etch client version') do |opt| + puts Etch::Client::VERSION + exit +end opts.on_tail("-h", "--help", "Show this message") do puts opts exit @@ -77,7 +75,7 @@ files_to_generate = opts.parse(ARGV) # Display a usage message if the user did not specify a valid action to perform. -unless (!files_to_generate.nil? && !files_to_generate.empty?) || $generateall +unless (!files_to_generate.nil? && !files_to_generate.empty?) || @generateall puts opts exit end @@ -86,7 +84,7 @@ # Do stuff # -etchclient = Etch::Client.new($server, $tag, $varbase, $debug, $dryrun, $interactive, $filenameonly, $fullfile) -status = etchclient.process_until_done(files_to_generate, $disableforce, $lockforce) +etchclient = Etch::Client.new(options) +status = etchclient.process_until_done(files_to_generate) exit status Modified: trunk/client/etch-client.spec =================================================================== --- trunk/client/etch-client.spec 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/etch-client.spec 2009-09-11 16:59:21 UTC (rev 87) @@ -1,6 +1,6 @@ Name: etch-client Summary: Etch client -Version: 1.1 +Version: VER Release: 1 Group: Applications/System License: MIT Modified: trunk/client/etch.rb =================================================================== --- trunk/client/etch.rb 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/etch.rb 2009-09-11 16:59:21 UTC (rev 87) @@ -2,9 +2,17 @@ # Etch configuration file management tool library ############################################################################## -require 'facter' +begin + # Try loading facter w/o gems first so that we don't introduce a + # dependency on gems if it is not needed. + require 'facter' # Facter +rescue LoadError + require 'rubygems' + require 'facter' +end require 'find' require 'digest/sha1' # hexdigest +require 'openssl' # OpenSSL require 'base64' # decode64, encode64 require 'uri' require 'net/http' @@ -14,24 +22,19 @@ require 'fcntl' # Fcntl::O_* require 'etc' # getpwnam, getgrnam require 'tempfile' # Tempfile +require 'cgi' +require 'timeout' -# clean up "using default DH parameters" warning for https -# http://blog.zenspider.com/2008/05/httpsssl-warning-cleanup.html -class Net::HTTP - alias :old_use_ssl= :use_ssl= - def use_ssl= flag - self.old_use_ssl = flag - @ssl_context.tmp_dh_callback = proc {} - end -end - module Etch end class Etch::Client + VERSION = '1.13' + CONFIRM_PROCEED = 1 CONFIRM_SKIP = 2 CONFIRM_QUIT = 3 + PRIVATE_KEY_PATHS = ["/etc/ssh/ssh_host_rsa_key", "/etc/ssh_host_rsa_key"] # We need these in relation to the output capturing ORIG_STDOUT = STDOUT.dup @@ -39,16 +42,18 @@ attr_reader :exec_once_per_run - # Cutting down the size of the arg list would be nice - def initialize(server=nil, tag=nil, varbase=nil, debug=false, dryrun=false, interactive=false, filenameonly=false, fullfile=false) - @server = server.nil? ? 'https://etch' : server - @tag = tag - @varbase = varbase.nil? ? '/var/etch' : varbase - @debug = debug - @dryrun = dryrun - @interactive = interactive - @filenameonly = filenameonly - @fullfile = fullfile + def initialize(options) + @server = options[:server] ? options[:server] : 'https://etch' + @tag = options[:tag] + @varbase = options[:varbase] ? options[:varbase] : '/var/etch' + @debug = options[:debug] + @dryrun = options[:dryrun] + @interactive = options[:interactive] + @filenameonly = options[:filenameonly] + @fullfile = options[:fullfile] + @key = options[:key] ? options[:key] : get_private_key_path + @disableforce = options[:disableforce] + @lockforce = options[:lockforce] # Ensure we have a sane path, particularly since we are often run from # cron. @@ -61,10 +66,17 @@ @origbase = File.join(@varbase, 'orig') @historybase = File.join(@varbase, 'history') @lockbase = File.join(@varbase, 'locks') + @requestbase = File.join(@varbase, 'requests') @blankrequest = {} @facts = Facter.to_hash + # If the user specified a non-standard key then override the sshrsakey + # fact so that authentication works + if @key + @facts['sshrsakey'] = IO.read(@key+'.pub').chomp.split[1] + end @facts.each_pair { |key, value| @blankrequest["facts[#{key}]"] = value.to_s } + @blankrequest['fqdn'] = @facts['fqdn'] if @facts['operatingsystemrelease'] # Some versions of Facter have a bug that leaves extraneous # whitespace on this fact. Work around that with strip. I.e. on @@ -90,8 +102,8 @@ @lchown_supported = nil @lchmod_supported = nil end - - def process_until_done(files_to_generate, disableforce, lockforce) + + def process_until_done(files_to_generate) # Our overall status. Will be reported to the server and used as the # return value for this method. Command-line clients should use it as # their exit value. Zero indicates no errors. @@ -100,6 +112,12 @@ http = Net::HTTP.new(@filesuri.host, @filesuri.port) if @filesuri.scheme == "https" + # Eliminate the OpenSSL "using default DH parameters" warning + if File.exist?('/etc/etch/dhparams') + dh = OpenSSL::PKey::DH.new(IO.read('/etc/etch/dhparams')) + Net::HTTP.ssl_context_accessor(:tmp_dh_callback) + http.tmp_dh_callback = proc { dh } + end http.use_ssl = true if File.exist?('/etc/etch/ca.pem') http.ca_file = '/etc/etch/ca.pem' @@ -115,14 +133,14 @@ # begin/raise for error events that end processing catch :stop_processing do begin - enabled, message = check_for_disable_etch_file(disableforce) + enabled, message = check_for_disable_etch_file if !enabled # 200 is the arbitrarily picked exit value indicating # that etch is disabled status = 200 throw :stop_processing end - remove_stale_lock_files(lockforce) + remove_stale_lock_files # Assemble the initial request request = get_blank_request @@ -130,6 +148,10 @@ if !files_to_generate.nil? && !files_to_generate.empty? files_to_generate.each do |file| request["files[#{CGI.escape(file)}][sha1sum]"] = get_orig_sum(file) + local_requests = get_local_requests(file) + if local_requests + request["files[#{CGI.escape(file)}][local_requests]"] = local_requests + end end else request['files[GENERATEALL]'] = '1' @@ -155,6 +177,7 @@ puts "Sending request to server #{@filesuri}" if (@debug) post = Net::HTTP::Post.new(@filesuri.path) post.set_form_data(request) + sign_post!(post, @key) response = http.request(post) response_xml = nil case response @@ -198,12 +221,20 @@ response_xml.root.elements.each('/files/need_sums/need_sum') do |need_sum| puts "Processing request for sum of #{need_sum.text}" if (@debug) request["files[#{CGI.escape(need_sum.text)}][sha1sum]"] = get_orig_sum(need_sum.text) + local_requests = get_local_requests(need_sum.text) + if local_requests + request["files[#{CGI.escape(need_sum.text)}][local_requests]"] = local_requests + end need_to_loop = true end response_xml.root.elements.each('/files/need_origs/need_orig') do |need_orig| puts "Processing request for contents of #{need_orig.text}" if (@debug) request["files[#{CGI.escape(need_orig.text)}][contents]"] = Base64.encode64(get_orig_contents(need_orig.text)) request["files[#{CGI.escape(need_orig.text)}][sha1sum]"] = get_orig_sum(need_orig.text) + local_requests = get_local_requests(need_orig.text) + if local_requests + request["files[#{CGI.escape(need_orig.text)}][local_requests]"] = local_requests + end need_to_loop = true end @@ -226,17 +257,18 @@ # Send results to server if !@dryrun rails_results = [] - # CGI.escape doesn't work on things that aren't strings, so we don't - # call it on a few of the fields here that are numbers or booleans + # A few of the fields here are numbers or booleans and need a + # to_s to make them compatible with CGI.escape, which expects a + # string. rails_results << "fqdn=#{CGI.escape(@facts['fqdn'])}" - rails_results << "status=#{status}" + rails_results << "status=#{CGI.escape(status.to_s)}" rails_results << "message=#{CGI.escape(message)}" @results.each do |result| # Strangely enough this works. Even though the key is not unique to # each result the Rails parameter parsing code keeps track of keys it # has seen, and if it sees a duplicate it starts a new hash. rails_results << "results[][file]=#{CGI.escape(result['file'])}" - rails_results << "results[][success]=#{result['success']}" + rails_results << "results[][success]=#{CGI.escape(result['success'].to_s)}" rails_results << "results[][message]=#{CGI.escape(result['message'])}" end puts "Sending results to server #{@resultsuri}" if (@debug) @@ -244,8 +276,10 @@ # We have to bypass Net::HTTP's set_form_data method in this case # because it expects a hash and we can't provide the results in the # format we want in a hash because we'd have duplicate keys (see above). - resultspost.body = rails_results.join('&') + results_as_string = rails_results.join('&') + resultspost.body = results_as_string resultspost.content_type = 'application/x-www-form-urlencoded' + sign_post!(resultspost, @key) response = http.request(resultspost) case response when Net::HTTPSuccess @@ -259,11 +293,11 @@ status end - def check_for_disable_etch_file(disableforce) + def check_for_disable_etch_file disable_etch = File.join(@varbase, 'disable_etch') message = '' if File.exist?(disable_etch) - if !disableforce + if !@disableforce message = "Etch disabled:\n" message << IO.read(disable_etch) puts message @@ -1464,7 +1498,39 @@ histrcspath = "#{histrcsdir}/#{histbase},v" File.chmod(histperms, histrcspath) if (!@dryrun) end - + + def get_local_requests(file) + requestdir = File.join(@requestbase, file) + requestlist = [] + if File.directory?(requestdir) + Dir.foreach(requestdir) do |entry| + next if entry == '.' + next if entry == '..' + requestfile = File.join(requestdir, entry) + request = IO.read(requestfile) + # Make sure it is valid XML + begin + request_xml = REXML::Document.new(request) + rescue REXML::ParseException => e + warn "Local request file #{requestfile} is not valid XML and will be ignored:\n" + e.message + next + end + # Make sure the root element is <request> + if request_xml.root.name != 'request' + warn "Local request file #{requestfile} is not properly formatted and will be ignored, XML root element is not <request>" + next + end + # Add it to the queue + requestlist << request + end + end + requests = nil + if !requestlist.empty? + requests = "<requests>\n#{requestlist.join('')}\n</requests>" + end + requests + end + # Haven't found a Ruby method for creating temporary directories, # so create a temporary file and replace it with a directory. def tempdir(file) @@ -1866,13 +1932,13 @@ # Any etch lockfiles more than a couple hours old are most likely stale # and can be removed. If told to force we remove all lockfiles. - def remove_stale_lock_files(force=false) + def remove_stale_lock_files twohoursago = Time.at(Time.now - 60 * 60 * 2) Find.find(@lockbase) do |file| next unless file =~ /\.LOCK$/ next unless File.file?(file) - if force || File.mtime(file) < twohoursago + if @lockforce || File.mtime(file) < twohoursago puts "Removing stale lock file #{file}" File.delete(file) end @@ -1883,6 +1949,10 @@ @already_processed.clear end + # We limit capturing to 5 minutes. That should be plenty of time + # for etch to handle any given file, including running any + # setup/pre/post commands. + OUTPUT_CAPTURE_TIMEOUT = 5 * 60 def start_output_capture # Establish a pipe, spawn a child process, and redirect stdout/stderr # to the pipe. The child gathers up anything sent over the pipe and @@ -1928,9 +1998,20 @@ # newline. $stdout.sync = true output = '' - while char = pread.getc - putc(char) - output << char.chr + begin + # A surprising number of apps that we restart are ill-behaved and do + # not properly close stdin/stdout/stderr. With etch's output + # capturing feature this results in etch hanging around forever + # waiting for the pipes to close. We time out after a suitable + # period of time so that etch processes don't hang around forever. + Timeout.timeout(OUTPUT_CAPTURE_TIMEOUT) do + while char = pread.getc + putc(char) + output << char.chr + end + end + rescue Timeout::Error + $stderr.puts "Timeout in output capture, some app restarted via post probably didn't daemonize properly" end pread.close owrite.write(output) @@ -1959,5 +2040,40 @@ Process.wait output end + + def get_private_key_path + key = nil + PRIVATE_KEY_PATHS.each do |path| + if File.readable?(path) + key = path + break + end + end + if !key + warn "No readable private key found, messages to server will not be signed and may be rejected depending on server configuration" + end + key + end + + # This method takes in a Net::HTTP::Post and a path to a private key. + # It will insert a 'timestamp' parameter to the post body, hash the body of + # the post, sign the hash using the private key, and insert that signature + # in the HTTP Authorization header field in the post. + def sign_post!(post, key) + if key + post.body << "×tamp=#{CGI.escape(Time.now.to_s)}" + private_key = OpenSSL::PKey::RSA.new(File.read(key)) + hashed_body = Digest::SHA1.hexdigest(post.body) + signature = Base64.encode64(private_key.private_encrypt(hashed_body)) + # encode64 breaks lines at 60 characters with newlines. Having newlines + # in an HTTP header screws things up (the lines get interpreted as + # separate headers) so strip them out. The Base64 standards seem to + # generally have a limit on line length, but Ruby's decode64 doesn't + # seem to complain. If it ever becomes a problem the server could + # rebreak the lines. + signature.gsub!("\n", '') + post['Authorization'] = "EtchSignature #{signature}" + end + end end Modified: trunk/client/pkginfo =================================================================== --- trunk/client/pkginfo 2009-06-25 00:13:39 UTC (rev 86) +++ trunk/client/pkginfo 2009-09-11 16:59:21 UTC (rev 87) @@ -1,7 +1,7 @@ PKG="OSSetch" NAME="Etch client" ARCH="sparc,i386" -VERSION="1.1" +VERSION="%VER%" CATEGORY="application" CLASSES="none" PSTAMP="none" This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |