Project

General

Profile

Download (7.68 KB) Statistics
| Branch: | Tag: | Revision:
require 'net/http'
require 'net/https'
require 'uri'
require 'time'
require 'fileutils'

require 'openssl'

require 'proxy/log'
require 'proxy/request'

module AbrtProxy
module Error
class Unauthorized < StandardError; end
class CertificateError < StandardError; end
class SyntaxError < StandardError; end
end

# Returns hex representation of random bytes-long number
def self.random_hex_string(nbytes)
OpenSSL::Random.random_bytes(nbytes).unpack('H*').join
end

# Generate multipart boundary separator
def self.suggest_separator
separator = "-"*28
separator + self.random_hex_string(16)
end

# It seems that Net::HTTP does not support multipart/form-data - this function
# is adapted from http://stackoverflow.com/a/213276 and lib/proxy/request.rb
def self.form_data_file(content, file_content_type)
# Assemble the request body using the special multipart format
thepart = "Content-Disposition: form-data; name=\"file\"; filename=\"*buffer*\"\r\n" +
"Content-Type: #{ file_content_type }\r\n\r\n#{ content }\r\n"

boundary = self.suggest_separator
while thepart.include? boundary
boundary = self.suggest_separator
end

body = "--" + boundary + "\r\n" + thepart + "--" + boundary + "--\r\n"
headers = {
"User-Agent" => "foreman-proxy/#{Proxy::VERSION}",
"Content-Type" => "multipart/form-data; boundary=#{ boundary }",
"Content-Length" => body.length.to_s
}

return headers, body
end

def self.faf_request(path, content, content_type="application/json")
uri = URI.parse(AbrtProxy::Plugin.settings.server_url.to_s)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.verify_mode =
if AbrtProxy::Plugin.settings.server_ssl_noverify
OpenSSL::SSL::VERIFY_NONE
else
OpenSSL::SSL::VERIFY_PEER
end

if AbrtProxy::Plugin.settings.server_ssl_cert && !AbrtProxy::Plugin.settings.server_ssl_cert.to_s.empty? \
&& AbrtProxy::Plugin.settings.server_ssl_key && !AbrtProxy::Plugin.settings.server_ssl_key.to_s.empty?
http.cert = OpenSSL::X509::Certificate.new(File.read(AbrtProxy::Plugin.settings.server_ssl_cert))
http.key = OpenSSL::PKey::RSA.new(File.read(AbrtProxy::Plugin.settings.server_ssl_key), nil)
end

headers, body = self.form_data_file content, content_type

path = [uri.path, path].join unless uri.path.empty?
response = http.start { |con| con.post(path, body, headers) }

response
end

def self.cert_names(request)
client_cert = request.env['SSL_CLIENT_CERT']
raise AbrtProxy::Error::Unauthorized, "Client certificate required" if client_cert.to_s.empty?

begin
client_cert = OpenSSL::X509::Certificate.new(client_cert)
rescue OpenSSL::OpenSSLError => e
raise AbrtProxy::Error::CertificateError, e.message
end

begin
cn = client_cert.subject.to_a.find { |name, value| name == 'CN' }
names = [cn[1]]
rescue NoMethodError
raise AbrtProxy::Error::CertificateError, "Common Name not found in the certificate"
end

alt_name_ext = client_cert.extensions.find { |ext| ext.oid == 'subjectAltName' }
if alt_name_ext
names += alt_name_ext.value.
split(/, ?/).
select { |s| s.start_with? 'URI:CN=' }.
map { |s| s.sub(/^URI:CN=/, '') }
end

return names
end

class AbrtRequest < Proxy::HttpRequest::ForemanRequest
def post_report(report)
send_request(request_factory.create_post('api/abrt_reports', report))
end
end

class HostReport
include Proxy::Log

class AggregatedReport
attr_accessor :report, :count, :hash, :reported_at
def initialize(report, count, hash, reported_at)
@report = report
@count = count
@hash = hash
@reported_at = Time.parse reported_at
end
end

class Error < RuntimeError; end

attr_reader :host, :reports, :files, :by_hash

def initialize(fname)
contents = IO.read(fname)
json = JSON.parse(contents)

[:report, :reported_at, :host].each do |field|
if !json.has_key?(field.to_s)
raise AbrtProxy::Error::SyntaxError, "Report #{fname} missing field #{field}"
end
end

report = json["report"]
hash = HostReport.duphash report
ar = AggregatedReport.new(json["report"], 1, hash, json["reported_at"])
@reports = [ar]
# index the array elements by duphash, if they have one
@by_hash = {}
@by_hash[hash] = ar unless hash.nil?
@files = [fname]
@host = json["host"]
@althosts = json["althosts"]
end

def merge(other)
raise HostReport::Error, "Host names do not match" unless @host == other.host

other.reports.each do |ar|
if !ar.hash.nil? && @by_hash.has_key?(ar.hash)
# we already have this report, just increment the counter
found_report = @by_hash[ar.hash]
found_report.count += ar.count
found_report.reported_at = [found_report.reported_at, ar.reported_at].min
else
# we either don't have this report or it has no hash
@reports << ar
@by_hash[ar.hash] = ar unless ar.hash.nil?
end
end
@files += other.files
end

def send_to_foreman
foreman_report = create_foreman_report
logger.debug "Sending #{foreman_report}"
AbrtRequest.new.post_report(foreman_report.to_json)
end

def unlink
@files.each do |fname|
logger.debug "Deleting #{fname}"
File.unlink(fname)
end
end

def self.save(hostnames, report, reported_at=nil)
# create the spool dir if it does not exist
FileUtils.mkdir_p HostReport.spooldir

reported_at ||= Time.now.utc
on_disk_report = { "host" => hostnames[0], "report" => report , "reported_at" => reported_at.to_s, "althosts" => hostnames[1..-1] }

# write report to temporary file
temp_fname = unique_filename "new-"
File.open temp_fname, File::WRONLY|File::CREAT|File::EXCL do |tmpfile|
tmpfile.write(on_disk_report.to_json)
end

# rename it
final_fname = unique_filename("ureport-" + DateTime.now.strftime("%FT%T") + "-")
File.link temp_fname, final_fname
File.unlink temp_fname
end

def self.load_from_spool
reports = []
report_files = Dir[File.join(HostReport.spooldir, "ureport-*")]
report_files.each do |fname|
begin
reports << new(fname)
rescue => e
logger.error "Failed to parse report #{fname}: #{e}"
end
end
reports
end

private

def format_reports
@reports.collect do |ar|
r = {
"count" => ar.count,
"reported_at" => ar.reported_at.utc.to_s,
"full" => ar.report
}
r["duphash"] = ar.hash unless ar.hash.nil?
r
end
end

def create_foreman_report
{ "abrt_report" => {
"host" => @host,
"althosts" => @althosts,
"reports" => format_reports
}
}
end

def self.duphash(report)
return nil if !AbrtProxy::Plugin.settings.aggregate_reports

begin
satyr_report = Satyr::Report.new report.to_json
stacktrace = satyr_report.stacktrace
thread = stacktrace.find_crash_thread
thread.duphash
rescue => e
logger.error "Error computing duphash: #{e}"
nil
end
end

def self.unique_filename(prefix)
File.join(HostReport.spooldir, prefix + AbrtProxy::random_hex_string(8))
end

def self.spooldir
AbrtProxy::Plugin.settings.spooldir
end
end
end
(2-2/5)