diff --git a/lib/nexpose/ajax.rb b/lib/nexpose/ajax.rb index 655a0aab..b63d2a7c 100644 --- a/lib/nexpose/ajax.rb +++ b/lib/nexpose/ajax.rb @@ -1,7 +1,5 @@ # encoding: utf-8 - module Nexpose - # Accessor to the Nexpose AJAX API. # These core methods should allow direct access to underlying controllers # in order to test functionality that is not currently exposed @@ -10,6 +8,8 @@ module Nexpose module AJAX module_function + # Content type strings acceptect by Nexpose. + # module CONTENT_TYPE XML = 'text/xml; charset=UTF-8' JSON = 'application/json; charset-utf-8' @@ -28,7 +28,7 @@ def get(nsc, uri, content_type = CONTENT_TYPE::XML, options = {}) parameterize_uri(uri, options) get = Net::HTTP::Get.new(uri) get.set_content_type(content_type) - _request(nsc, get) + request(nsc, get) end # PUT call to a Nexpose controller. @@ -43,7 +43,7 @@ def put(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) put = Net::HTTP::Put.new(uri) put.set_content_type(content_type) put.body = payload.to_s if payload - _request(nsc, put) + request(nsc, put) end # POST call to a Nexpose controller. @@ -52,13 +52,14 @@ def put(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) # @param [String] uri Controller address relative to https://host:port # @param [String|REXML::Document] payload XML document required by the call. # @param [String] content_type Content type to use when issuing the POST. + # @param [Fixnum] timeout Set an explicit timeout for the HTTP request. # @return [String|REXML::Document|Hash] The response from the call. # - def post(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) + def post(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML, timeout = nil) post = Net::HTTP::Post.new(uri) post.set_content_type(content_type) post.body = payload.to_s if payload - _request(nsc, post) + request(nsc, post, timeout) end # PATCH call to a Nexpose controller. @@ -73,7 +74,7 @@ def patch(nsc, uri, payload = nil, content_type = CONTENT_TYPE::XML) patch = Net::HTTP::Patch.new(uri) patch.set_content_type(content_type) patch.body = payload.to_s if payload - _request(nsc, patch) + request(nsc, patch) end # POST call to a Nexpose controller that uses a form-post model. @@ -90,7 +91,7 @@ def form_post(nsc, uri, parameters, content_type = CONTENT_TYPE::FORM) post = Net::HTTP::Post.new(uri) post.set_content_type(content_type) post.set_form_data(parameters) - _request(nsc, post) + request(nsc, post) end # DELETE call to a Nexpose controller. @@ -101,9 +102,16 @@ def form_post(nsc, uri, parameters, content_type = CONTENT_TYPE::FORM) def delete(nsc, uri, content_type = CONTENT_TYPE::XML) delete = Net::HTTP::Delete.new(uri) delete.set_content_type(content_type) - _request(nsc, delete) + request(nsc, delete) end + ### + # === Internal helper methods below this line. === + # + # These are internal utility methods, not subject to backward compatibility + # concerns. + ### + # Append the query parameters to given URI. # # @param [String] uri Controller address relative to https://host:port @@ -114,59 +122,93 @@ def delete(nsc, uri, content_type = CONTENT_TYPE::XML) def parameterize_uri(uri, parameters) params = Hash.try_convert(parameters) unless params.nil? || params.empty? - uri = uri.concat(('?').concat(parameters.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&'))) + uri = uri.concat(('?').concat(parameters.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&'))) end uri end - def preserving_preference(nsc, pref) - begin - orig = _get_rows(nsc, pref) - yield - ensure - _set_rows(nsc, pref, orig) - end - end - - ### - # Internal helper methods - # Use the Nexpose::Connection to establish a correct HTTPS object. - def _https(nsc) + def https(nsc, timeout = nil) http = Net::HTTP.new(nsc.host, nsc.port) + http.read_timeout = timeout if timeout http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE http end # Attach necessary header fields. - def _headers(nsc, request) + def headers(nsc, request) request.add_field('nexposeCCSessionID', nsc.session_id) request.add_field('Cookie', "nexposeCCSessionID=#{nsc.session_id}") end - def _request(nsc, request) - http = _https(nsc) - _headers(nsc, request) + def request(nsc, request, timeout = nil) + http = https(nsc, timeout) + headers(nsc, request) # Return response body if request is successful. Brittle. response = http.request(request) case response - when Net::HTTPOK - response.body - when Net::HTTPCreated + when Net::HTTPOK, Net::HTTPCreated response.body when Net::HTTPForbidden raise Nexpose::PermissionError.new(response) - when Net::HTTPUnauthorized - raise Nexpose::PermissionError.new(response) + when Net::HTTPFound + if response.header['location'] =~ /login/ + raise Nexpose::AuthenticationFailed.new(response) + else + req_type = request.class.name.split('::').last.upcase + raise Nexpose::APIError.new(response, "#{req_type} request to #{request.path} failed. #{request.body}", response.code) + end else req_type = request.class.name.split('::').last.upcase raise Nexpose::APIError.new(response, "#{req_type} request to #{request.path} failed. #{request.body}", response.code) end end - def _get_rows(nsc, pref) + # Execute a block of code while presenving the preferences for any + # underlying table being accessed. Use this method when accessing data + # tables which are present in the UI to prevent existing row preferences + # from being set to 500. + # + # This is an internal utility method, not subject to backward compatibility + # concerns. + # + # @param [Connection] nsc Live connection to a Nepose console. + # @param [String] pref Preference key value to preserve. + # + def preserving_preference(nsc, pref) + begin + orig = get_rows(nsc, pref) + yield + ensure + set_rows(nsc, pref, orig) + end + end + + # Get a valid row preference value. + # + # This is an internal utility method, not subject to backward compatibility + # concerns. + # + # @param [Fixnum] val Value to get inclusive row preference for. + # @return [Fixnum] Valid row preference. + # + def row_pref_of(val) + if val.nil? || val > 100 + 500 + elsif val > 50 + 100 + elsif val > 25 + 50 + elsif val > 10 + 25 + else + 10 + end + end + + def get_rows(nsc, pref) uri = '/ajax/user_pref_get.txml' resp = get(nsc, uri, CONTENT_TYPE::XML, 'name' => "#{pref}.rows") xml = REXML::Document.new(resp) @@ -178,7 +220,7 @@ def _get_rows(nsc, pref) end end - def _set_rows(nsc, pref, value) + def set_rows(nsc, pref, value) uri = '/ajax/user_pref_set.txml' params = { 'name' => "#{pref}.rows", 'value' => value } @@ -188,19 +230,5 @@ def _set_rows(nsc, pref, value) attr.value == '1' end end - - def _row_pref_of(val) - if val.nil? || val > 100 - 500 - elsif val > 50 - 100 - elsif val > 25 - 50 - elsif val > 10 - 25 - else - 10 - end - end end end diff --git a/lib/nexpose/scan.rb b/lib/nexpose/scan.rb index cda7522b..9ff287d9 100644 --- a/lib/nexpose/scan.rb +++ b/lib/nexpose/scan.rb @@ -238,7 +238,7 @@ def scan_statistics(scan_id) # def past_scans(limit = nil) uri = '/data/scan/global/scan-history' - rows = AJAX._row_pref_of(limit) + rows = AJAX.row_pref_of(limit) params = { 'sort' => 'endTime', 'dir' => 'DESC', 'startIndex' => 0 } AJAX.preserving_preference(self, 'global-completed-scans') do data = DataTable._get_json_table(self, uri, params, rows, limit) @@ -255,7 +255,7 @@ def past_scans(limit = nil) # zip_file, if provided. Otherwise, returns raw ZIP binary data. # def export_scan(scan_id, zip_file = nil) - http = AJAX._https(self) + http = AJAX.https(self) headers = { 'Cookie' => "nexposeCCSessionID=#{@session_id}", 'Accept-Encoding' => 'identity' } resp = http.get("/data/scan/#{scan_id}/export", headers) @@ -305,8 +305,8 @@ def import_scan(site_id, zip_file) post.set_content_type('multipart/form-data', boundary: data.bound) # Avoiding AJAX#request, because the data can cause binary dump on error. - http = AJAX._https(self) - AJAX._headers(self, post) + http = AJAX.https(self) + AJAX.headers(self, post) response = http.request(post) case response when Net::HTTPOK