lib/toadhopper.rb in toadhopper-2.0 vs lib/toadhopper.rb in toadhopper-2.1
- old
+ new
@@ -1,26 +1,83 @@
-require 'net/http'
+require 'net/https'
require 'erb'
require 'ostruct'
+require 'toadhopper_exception'
# Posts errors to the Airbrake API
class Toadhopper
- VERSION = "2.0"
- FILTER_REPLACEMENT = "[FILTERED]"
+ VERSION = '2.1'
+ FILTER_REPLACEMENT = "[FILTERED]"
+ DEFAULT_DOMAIN = 'airbrake.io'
+ DEFAULT_NOTIFY_HOST = 'http://'+DEFAULT_DOMAIN
+ # CA_FILE: Path to an updated certificate authority file, which was built from source
+ # If you provide a custom Net :transport and get erroneous SSL peer verification failures,
+ # try setting the transport's ca_file to Toadhopper::CA_FILE
+ # @see https://github.com/toolmantim/toadhopper/blob/master/resources/README.md
+ CA_FILE = File.expand_path File.join('..', 'resources', 'ca-bundle.crt'),
+ File.dirname(__FILE__)
# Airbrake API response
class Response < Struct.new(:status, :body, :errors); end
- attr_reader :api_key
+ attr_reader :api_key, :error_url, :deploy_url
+ # Initialize and configure a Toadhopper
+ #
+ # @param [String] Your api key
+ # @param [Hash] params [optional]
+ #
+ # :notify_host - [String] The default host to use
+ # :error_url - [String] Absolute URL to use for error reporting
+ # :deploy_url - [String] Absolute URL to use for deploy tracking
+ # :transport - [Net::HTTP|Net::HTTP::Proxy] A customized Net::* object
def initialize(api_key, params = {})
- @api_key = api_key
- @notify_host = params.delete(:notify_host) || "http://airbrakeapp.com"
- @error_url = params.delete(:error_url) || "#{@notify_host}/notifier_api/v2/notices"
- @deploy_url = params.delete(:deploy_url) || "#{@notify_host}/deploys.txt"
+ @filters = []
+ @api_key = api_key
+
+ notify_host = URI.parse(params[:notify_host] || DEFAULT_NOTIFY_HOST)
+ @transport = params.delete :transport
+ if @transport and not params[:notify_host]
+ notify_host.scheme = 'https' if @transport.use_ssl?
+ notify_host.host = @transport.address
+ notify_host.port = @transport.port
+ end
+
+ @error_url = URI.parse(params.delete(:error_url) || "#{notify_host}/notifier_api/v2/notices")
+ @deploy_url = URI.parse(params.delete(:deploy_url) || "#{notify_host}/deploys.txt")
+
+ validate!
end
+ def validate!
+ validate_url! :error_url
+ validate_url! :deploy_url
+ end
+
+ def validate_url!(sym)
+ url = instance_variable_get '@'+sym.to_s
+ unless url.absolute?
+ raise ToadhopperException, "#{sym} #{url.inspect} must begin with http:// or https://"
+ end
+
+ if @transport
+ if @transport.use_ssl? != url.scheme.eql?('https')
+ raise ToadhopperException,
+ ":transport use_ssl? setting of #{@transport.use_ssl?.inspect} does not match" +
+ " #{sym} scheme #{url.scheme.inspect}"
+ elsif @transport.address != url.host
+ raise ToadhopperException,
+ ":transport hostname #{@transport.address.inspect} does not match" +
+ " #{sym} hostname #{url.host.inspect}"
+ elsif @transport.port != url.port
+ raise ToadhopperException,
+ ":transport port #{@transport.port.inspect} does not match" +
+ " #{sym} port #{url.port.inspect}"
+ end
+ end
+ end
+
# Sets patterns to +[FILTER]+ out sensitive data such as +/password/+, +/email/+ and +/credit_card_number/+
def filters=(*filters)
@filters = filters.flatten
end
@@ -69,14 +126,37 @@
params['api_key'] = @api_key
params['deploy[rails_env]'] = options[:framework_env] || 'development'
params['deploy[local_username]'] = options[:username] || %x(whoami).strip
params['deploy[scm_repository]'] = options[:scm_repository]
params['deploy[scm_revision]'] = options[:scm_revision]
- response = Net::HTTP.post_form(URI.parse(@deploy_url), params)
- parse_response(response)
+ response(@deploy_url, params)
end
+ def secure?
+ connection(@deploy_url).use_ssl? and connection(@error_url).use_ssl?
+ end
+
+ # Provider of the net transport used
+ #
+ # MIT Licensing Note: Portions of logic below for connecting via SSL were
+ # copied from the airbrake project under the MIT License.
+ #
+ # @see https://github.com/airbrake/airbrake/blob/master/MIT-LICENSE
+ def connection(uri)
+ return @transport if @transport
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.read_timeout = 5 # seconds
+ http.open_timeout = 2 # seconds
+ if uri.scheme.eql? 'https'
+ http.use_ssl = true
+ http.ca_file = CA_FILE
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+ http
+ end
+
private
def document_defaults(error)
{
:error => error,
@@ -105,28 +185,53 @@
def filters
[@filters].flatten.compact
end
def post_document(document, headers={})
- uri = URI.parse(@error_url)
- Net::HTTP.start(uri.host, uri.port) do |http|
- http.read_timeout = 5 # seconds
- http.open_timeout = 2 # seconds
+ all_headers = {'Content-type' => 'text/xml', 'Accept' => 'text/xml, application/xml'}.merge(headers)
+ response(@error_url, document, all_headers)
+ end
+
+ def response(uri, data, headers=nil)
+ connection(uri).start do |http|
begin
- response = http.post uri.path,
- document,
- {'Content-type' => 'text/xml', 'Accept' => 'text/xml, application/xml'}.merge(headers)
+ # If data is Hash-like, we post it as a form
+ response = if data.respond_to? :has_key?
+ # Post url-encoded form data
+ request = Net::HTTP::Post.new(uri.path)
+ request.form_data = data
+ http.request(request)
+ else
+ # Post a basic body of data
+ http.post uri.path, data, headers
+ end
parse_response(response)
rescue TimeoutError => e
Response.new(500, '', ['Timeout error'])
end
end
end
def parse_response(response)
+ if response.body.include? '</'
+ parse_xml_response(response)
+ else
+ parse_text_response(response)
+ end
+ end
+
+ def parse_xml_response(response)
Response.new(response.code.to_i,
response.body,
response.body.scan(%r{<error>(.+)<\/error>}).flatten)
+ end
+
+ def parse_text_response(response)
+ errors = []
+ unless response.kind_of? Net::HTTPSuccess or response.body.to_s.empty?
+ errors << response.body
+ end
+ Response.new(response.code.to_i, response.body, errors)
end
def document_for(exception, options={})
data = document_data(exception, options)
scope = OpenStruct.new(data).extend(ERB::Util)