-
Notifications
You must be signed in to change notification settings - Fork 103
/
Copy pathapi_request.rb
152 lines (136 loc) · 5.68 KB
/
api_request.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
module Nexpose
class APIRequest
include XMLUtils
attr_reader :http
attr_reader :uri
attr_reader :headers
attr_reader :req
attr_reader :res
attr_reader :sid
attr_reader :success
attr_reader :error
attr_reader :trace
attr_reader :raw_response
attr_reader :raw_response_data
attr_reader :trust_store
def initialize(req, url, api_version = '1.1', trust_store = nil)
@url = url
@req = req
@api_version = api_version
@url = @url.sub('API_VERSION', @api_version)
@trust_store = trust_store
prepare_http_client
end
def prepare_http_client
@uri = URI.parse(@url)
@http = Net::HTTP.new(@uri.host, @uri.port)
@http.use_ssl = true
#
# XXX: This is obviously a security issue, however, we handle this at the client level by forcing
# a confirmation when the nexpose host is not localhost. In a perfect world, we would present
# the server signature before accepting it, but this requires either a direct callback inside
# of this module back to whatever UI, or opens a race condition between accept and attempt.
if @trust_store.nil?
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
else
@http.cert_store = @trust_store
end
@headers = { 'Content-Type' => 'text/xml' }
@success = false
end
def execute(options = {})
@conn_tries = 0
begin
prepare_http_client
@http.read_timeout = options.key?(:timeout) ? options[:timeout] : 120
@http.open_timeout = options.key?(:open_timeout) ? options[:open_timeout] : 120
@raw_response = @http.post(@uri.path, @req, @headers)
@raw_response_data = @raw_response.read_body
# Allow the :raw keyword to bypass XML parsing.
if options[:raw]
if raw_response_data =~ /success="1"/
@success = true
else
@success = false
@error = 'User requested raw XML response. Not parsing failures.'
end
else
@res = parse_xml(@raw_response_data)
unless @res.root
@error = 'Nexpose service returned invalid XML.'
return @sid
end
@sid = attributes['session-id']
if (attributes['success'] && attributes['success'].to_i == 1)
@success = true
elsif @api_version =~ /1.2/ && @res && (@res.get_elements '//Exception').count < 1
@success = true
else
@success = false
if @api_version =~ /1.2/
@res.elements.each('//Exception/Message') do |message|
@error = message.text.sub(/.*Exception: */, '')
end
@res.elements.each('//Exception/Stacktrace') do |stacktrace|
@trace = stacktrace.text
end
else
@res.elements.each('//message') do |message|
@error = message.text.sub(/.*Exception: */, '')
end
@res.elements.each('//stacktrace') do |stacktrace|
@trace = stacktrace.text
end
end
end
end
# This is a hack to handle corner cases where a heavily loaded Nexpose instance
# drops our HTTP connection before processing. We try 5 times to establish a
# connection in these situations. The actual exception occurs in the Ruby
# http library, which is why we use such generic error classes.
rescue OpenSSL::SSL::SSLError => error
if @conn_tries < 5
@conn_tries += 1
$stderr.puts "\n\nRetrying the request due to #{error}. If you see this message please open an Issue on Github with the error.\n\n"
retry
end
rescue ::ArgumentError, ::NoMethodError => error
if @conn_tries < 5
@conn_tries += 1
$stderr.puts "\n\nRetrying the request due to #{error}. If you see this message please open an Issue on Github with the error.\n\n"
retry
end
### HTTP Specific Timeout Errors.
rescue ::Net::ReadTimeout, ::Net::OpenTimeout => error
timeout_value = error.instance_of?(Net::ReadTimeout) ? @http.read_timeout : @http.open_timeout
@error = "Nexpose did not respond within #{timeout_value} seconds #{error}. Reference the Wiki for information on setting the different Timeout values."
### Catch all Timeout Error.
rescue ::Timeout::Error => error
@error = "Nexpose did not respond within #{@http.read_timeout} seconds #{error}. Reference the Wiki for information on setting the different Timeout values."
rescue ::Errno::EHOSTUNREACH, ::Errno::ENETDOWN, ::Errno::ENETUNREACH, ::Errno::ENETRESET, ::Errno::EHOSTDOWN, ::Errno::EACCES, ::Errno::EINVAL, ::Errno::EADDRNOTAVAIL
@error = 'Nexpose host is unreachable.'
# Handle console-level interrupts
rescue ::Interrupt
@error = 'Received a user interrupt.'
rescue ::Errno::ECONNRESET, ::Errno::ECONNREFUSED, ::Errno::ENOTCONN, ::Errno::ECONNABORTED
@error = 'Nexpose service is not available.'
rescue ::REXML::ParseException => exc
@error = "Error parsing response: #{exc.message}"
end
if !(@success || @error)
@error = "Nexpose service returned an unrecognized response: #{@raw_response_data.inspect}"
end
@sid
end
def attributes(*args)
return unless @res.root
@res.root.attributes(*args)
end
def self.execute(url, req, api_version = '1.1', options = {}, trust_store = nil)
obj = self.new(req.to_s, url, api_version, trust_store)
obj.execute(options)
raise APIError.new(obj, "Action failed: #{obj.error}") unless obj.success
obj
end
end
end