diff --git a/nuget/lib/dependabot/nuget/http_response_helpers.rb b/nuget/lib/dependabot/nuget/http_response_helpers.rb new file mode 100644 index 0000000000..2dc6bc5858 --- /dev/null +++ b/nuget/lib/dependabot/nuget/http_response_helpers.rb @@ -0,0 +1,14 @@ +# typed: true +# frozen_string_literal: true + +module Dependabot + module Nuget + module HttpResponseHelpers + def self.remove_wrapping_zero_width_chars(string) + string.force_encoding("UTF-8").encode + .gsub(/\A[\u200B-\u200D\uFEFF]/, "") + .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/nuget_client.rb b/nuget/lib/dependabot/nuget/nuget_client.rb index e0e6f4238d..f2292956cc 100644 --- a/nuget/lib/dependabot/nuget/nuget_client.rb +++ b/nuget/lib/dependabot/nuget/nuget_client.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/nuget/cache_manager" +require "dependabot/nuget/http_response_helpers" require "dependabot/nuget/update_checker/repository_finder" require "sorbet-runtime" @@ -162,7 +163,7 @@ def self.get_package_versions(dependency_name, repository_details) ) return unless response.status == 200 - body = remove_wrapping_zero_width_chars(response.body) + body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) JSON.parse(body) end @@ -193,13 +194,6 @@ def self.get_package_versions(dependency_name, repository_details) raise PrivateSourceTimedOut, repo_url end - - sig { params(string: String).returns(String) } - private_class_method def self.remove_wrapping_zero_width_chars(string) - string.force_encoding("UTF-8").encode - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb index 2f937e088e..ddf4bb6181 100644 --- a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +++ b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb @@ -4,6 +4,7 @@ require "nokogiri" require "zip" require "stringio" +require "dependabot/nuget/http_response_helpers" module Dependabot module Nuget @@ -43,11 +44,48 @@ def self.fetch_nupkg_buffer_from_repository(repository_details, package_id, pack end def self.get_nuget_v3_package_url(repository_details, package_id, package_version) - base_url = repository_details[:base_url].delete_suffix("/") + base_url = repository_details[:base_url] + unless base_url + return get_nuget_v3_package_url_from_search(repository_details, package_id, + package_version) + end + + base_url = base_url.delete_suffix("/") package_id_downcased = package_id.downcase "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.#{package_version}.nupkg" end + # rubocop:disable Metrics/PerceivedComplexity + def self.get_nuget_v3_package_url_from_search(repository_details, package_id, package_version) + search_url = repository_details[:search_url] + return nil unless search_url + + # get search result + search_result_response = fetch_url(search_url, repository_details) + return nil unless search_result_response.status == 200 + + search_response_body = HttpResponseHelpers.remove_wrapping_zero_width_chars(search_result_response.body) + search_results = JSON.parse(search_response_body) + + # find matching package and version + package_search_result = search_results&.[]("data")&.find { |d| package_id.casecmp?(d&.[]("id")) } + version_search_result = package_search_result&.[]("versions")&.find do |v| + package_version.casecmp?(v&.[]("version")) + end + registration_leaf_url = version_search_result&.[]("@id") + + registration_leaf_response = fetch_url(registration_leaf_url, repository_details) + return nil unless registration_leaf_response.status == 200 + + registration_leaf_response_body = + HttpResponseHelpers.remove_wrapping_zero_width_chars(registration_leaf_response.body) + registration_leaf = JSON.parse(registration_leaf_response_body) + + # finally, get the .nupkg url + registration_leaf&.[]("packageContent") + end + # rubocop:enable Metrics/PerceivedComplexity + def self.get_nuget_v2_package_url(feed_url, package_id, package_version) base_url = feed_url base_url += "/" unless base_url.end_with?("/") @@ -86,6 +124,16 @@ def self.fetch_stream(stream_url, auth_header, max_redirects = 5) end end end + + def self.fetch_url(url, repository_details) + cache = CacheManager.cache("nupkg_fetcher_cache") + cache[url] ||= Dependabot::RegistryClient.get( + url: url, + headers: repository_details.fetch(:auth_header) + ) + + cache[url] + end end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb index ea1ca79055..e8cbad3beb 100644 --- a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb @@ -7,6 +7,7 @@ require "dependabot/update_checkers/base" require "dependabot/registry_client" require "dependabot/nuget/cache_manager" +require "dependabot/nuget/http_response_helpers" module Dependabot module Nuget @@ -75,15 +76,14 @@ def build_url_for_details(repo_details) check_repo_response(response, repo_details) return unless response.status == 200 - body = remove_wrapping_zero_width_chars(response.body) + body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) parsed_json = JSON.parse(body) base_url = base_url_from_v3_metadata(parsed_json) - resolved_base_url = base_url || repo_details.fetch(:url).gsub("/index.json", "-flatcontainer") search_url = search_url_from_v3_metadata(parsed_json) registration_url = registration_url_from_v3_metadata(parsed_json) details = { - base_url: resolved_base_url, + base_url: base_url, repository_url: repo_details.fetch(:url), auth_header: auth_header_for_token(repo_details.fetch(:token)), repository_type: "v3" @@ -330,12 +330,6 @@ def expand_windows_style_environment_variables(string) end end - def remove_wrapping_zero_width_chars(string) - string.force_encoding("UTF-8").encode - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end - def auth_header_for_token(token) return {} unless token diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb index f631503e8b..f68109a4df 100644 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb @@ -81,6 +81,82 @@ it { is_expected.to eq("https://nuget.pkg.github.com/some-namespace/download/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } end + + context "from a v3 feed that doesn't specify `PackageBaseAddress`" do + let(:feed_url) { "https://nuget.example.com/v3-without-package-base/index.json" } + + before do + # initial `index.json` response; only provides `SearchQueryService` and not `PackageBaseAddress` + stub_request(:get, feed_url) + .to_return( + status: 200, + body: { + version: "3.0.0", + resources: [ + { + "@id" => "https://nuget.example.com/query", + "@type" => "SearchQueryService" + } + ] + }.to_json + ) + # SearchQueryService + stub_request(:get, "https://nuget.example.com/query?q=newtonsoft.json&prerelease=true&semVerLevel=2.0.0") + .to_return( + status: 200, + body: { + totalHits: 2, + data: [ + # this is a false match + { + registration: "not-used", + version: "42.42.42", + versions: [ + { + version: "1.0.0", + "@id" => "not-used" + }, + { + version: "42.42.42", + "@id" => "not-used" + } + ], + id: "Newtonsoft.Json.False.Match" + }, + # this is the real one + { + registration: "not-used", + version: "13.0.1", + versions: [ + { + version: "12.0.1", + "@id" => "https://nuget.example.com/registration/newtonsoft.json/12.0.1.json" + }, + { + version: "13.0.1", + "@id" => "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json" + } + ], + id: "Newtonsoft.Json" + } + ] + }.to_json + ) + # registration content + stub_request(:get, "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json") + .to_return( + status: 200, + body: { + listed: true, + packageContent: "https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg", + registration: "not-used", + "@id" => "not-used" + }.to_json + ) + end + + it { is_expected.to eq("https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg") } + end end describe "#fetch_nupkg_buffer" do diff --git a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb index 99a1d1a10c..a15b7cd796 100644 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb @@ -167,7 +167,7 @@ it "gets the right URL" do expect(dependency_urls).to eq( [{ - base_url: "http://localhost:8082/artifactory/api/nuget/v3/nuget-local", + base_url: nil, registration_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ "dependabot-nuget-local/registration/microsoft.extensions.dependencymodel/index.json", repository_url: custom_repo_url,