Skip to content

Commit 8c584b9

Browse files
committed
standardize package fetcher for maven
1 parent 0f198ea commit 8c584b9

File tree

2 files changed

+269
-190
lines changed

2 files changed

+269
-190
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "json"
5+
require "time"
6+
require "cgi"
7+
require "excon"
8+
require "nokogiri"
9+
require "sorbet-runtime"
10+
require "dependabot/registry_client"
11+
require "dependabot/python/name_normaliser"
12+
require "dependabot/package/package_release"
13+
require "dependabot/package/package_details"
14+
require "dependabot/python/package/package_registry_finder"
15+
16+
# Stores metadata for a package, including all its available versions
17+
module Dependabot
18+
module Maven
19+
module Package
20+
class PackageDetailsFetcher
21+
extend T::Sig
22+
23+
sig do
24+
params(
25+
dependency: Dependabot::Dependency,
26+
dependency_files: T::Array[Dependabot::DependencyFile],
27+
credentials: T::Array[Dependabot::Credential]
28+
).void
29+
end
30+
def initialize(
31+
dependency:,
32+
dependency_files:,
33+
credentials:
34+
)
35+
@dependency = dependency
36+
@dependency_files = dependency_files
37+
@credentials = credentials
38+
39+
@registry_urls = T.let(nil, T.nilable(T::Array[String]))
40+
@forbidden_urls = T.let([], T::Array[String])
41+
@pom_repository_details = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
42+
@dependency_metadata = T.let({}, T::Hash[T.untyped, Nokogiri::XML::Document])
43+
@repository_finder = T.let(nil, T.nilable(Maven::FileParser::RepositoriesFinder))
44+
@repositories = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
45+
@released_check = T.let({}, T::Hash[Version, T::Boolean])
46+
@auth_headers_finder = T.let(nil, T.nilable(Utils::AuthHeadersFinder))
47+
end
48+
49+
sig { returns(Dependabot::Dependency) }
50+
attr_reader :dependency
51+
52+
sig { returns(T::Array[T.untyped]) }
53+
attr_reader :dependency_files
54+
55+
sig { returns(T::Array[T.untyped]) }
56+
attr_reader :credentials
57+
sig { returns(T::Array[T.untyped]) }
58+
attr_reader :forbidden_urls
59+
60+
sig { returns(T::Array[T.untyped]) }
61+
def versions
62+
version_details =
63+
repositories.map do |repository_details|
64+
url = repository_details.fetch("url")
65+
xml = dependency_metadata(repository_details)
66+
next [] if xml.nil?
67+
68+
break xml.css("versions > version")
69+
.select { |node| version_class.correct?(node.content) }
70+
.map { |node| version_class.new(node.content) }
71+
.map { |version| { version: version, source_url: url } }
72+
end.flatten
73+
74+
raise PrivateSourceAuthenticationFailure, forbidden_urls.first if version_details.none? && forbidden_urls.any?
75+
76+
version_details.sort_by { |details| details.fetch(:version) }
77+
end
78+
79+
sig { params(version: Version).returns(T::Boolean) }
80+
def released?(version)
81+
@released_check[version] ||=
82+
repositories.any? do |repository_details|
83+
url = repository_details.fetch("url")
84+
response = Dependabot::RegistryClient.head(
85+
url: dependency_files_url(url, version),
86+
headers: repository_details.fetch("auth_headers")
87+
)
88+
89+
response.status < 400
90+
rescue Excon::Error::Socket, Excon::Error::Timeout,
91+
Excon::Error::TooManyRedirects
92+
false
93+
rescue URI::InvalidURIError => e
94+
raise DependencyFileNotResolvable, e.message
95+
end
96+
end
97+
98+
private
99+
100+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
101+
def repositories
102+
return @repositories if @repositories
103+
104+
@repositories = credentials_repository_details
105+
pom_repository_details.each do |repo|
106+
@repositories << repo unless @repositories.any? { |r| r["url"] == repo["url"] }
107+
end
108+
@repositories
109+
end
110+
111+
sig { params(repository_details: T::Hash[String, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) }
112+
def dependency_metadata(repository_details)
113+
repository_key = repository_details.hash
114+
return @dependency_metadata[repository_key] if @dependency_metadata.key?(repository_key)
115+
116+
xml_document = fetch_dependency_metadata(repository_details)
117+
118+
@dependency_metadata[repository_key] ||= xml_document if xml_document
119+
@dependency_metadata[repository_key]
120+
end
121+
122+
sig { params(repository_details: T::Hash[String, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) }
123+
def fetch_dependency_metadata(repository_details)
124+
response = Dependabot::RegistryClient.get(
125+
url: dependency_metadata_url(repository_details.fetch("url")),
126+
headers: repository_details.fetch("auth_headers")
127+
)
128+
check_response(response, repository_details.fetch("url"))
129+
return unless response.status < 400
130+
131+
Nokogiri::XML(response.body)
132+
rescue URI::InvalidURIError
133+
nil
134+
rescue Excon::Error::Socket, Excon::Error::Timeout,
135+
Excon::Error::TooManyRedirects => e
136+
137+
if central_repo_urls.include?(repository_details["url"])
138+
response_status = response&.status || 0
139+
response_body = if response
140+
"RegistryError: #{response.status} response status with body #{response.body}"
141+
else
142+
"RegistryError: #{e.message}"
143+
end
144+
145+
raise RegistryError.new(response_status, response_body)
146+
end
147+
148+
nil
149+
end
150+
151+
sig { params(response: Excon::Response, repository_url: String).void }
152+
def check_response(response, repository_url)
153+
return unless [401, 403].include?(response.status)
154+
return if @forbidden_urls.include?(repository_url)
155+
return if central_repo_urls.include?(repository_url)
156+
157+
@forbidden_urls << repository_url
158+
end
159+
160+
sig { returns(Maven::FileParser::RepositoriesFinder) }
161+
def repository_finder
162+
return @repository_finder if @repository_finder
163+
164+
@repository_finder =
165+
Maven::FileParser::RepositoriesFinder.new(
166+
pom_fetcher: Maven::FileParser::PomFetcher.new(dependency_files: dependency_files),
167+
dependency_files: dependency_files,
168+
credentials: credentials
169+
)
170+
@repository_finder
171+
end
172+
173+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
174+
def pom_repository_details
175+
return @pom_repository_details if @pom_repository_details
176+
177+
@pom_repository_details =
178+
repository_finder
179+
.repository_urls(pom: pom)
180+
.map do |url|
181+
{ "url" => url, "auth_headers" => {} }
182+
end
183+
@pom_repository_details
184+
end
185+
186+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
187+
def pom
188+
filename = dependency.requirements.first&.fetch(:file)
189+
dependency_files.find { |f| f.name == filename }
190+
end
191+
192+
sig { params(repository_url: String).returns(String) }
193+
def dependency_metadata_url(repository_url)
194+
group_id, artifact_id = dependency.name.split(":")
195+
196+
"#{repository_url}/" \
197+
"#{group_id&.tr('.', '/')}/" \
198+
"#{artifact_id}/" \
199+
"maven-metadata.xml"
200+
end
201+
202+
sig { params(repository_url: String, version: Version).returns(String) }
203+
def dependency_files_url(repository_url, version)
204+
group_id, artifact_id = dependency.name.split(":")
205+
type = dependency.requirements.first&.dig(:metadata, :packaging_type)
206+
classifier = dependency.requirements.first&.dig(:metadata, :classifier)
207+
208+
actual_classifier = classifier.nil? ? "" : "-#{classifier}"
209+
"#{repository_url}/" \
210+
"#{group_id&.tr('.', '/')}/" \
211+
"#{artifact_id}/" \
212+
"#{version}/" \
213+
"#{artifact_id}-#{version}#{actual_classifier}.#{type}"
214+
end
215+
216+
sig { returns(T::Array[T.untyped]) }
217+
def credentials_repository_details
218+
credentials
219+
.select { |cred| cred["type"] == "maven_repository" && cred["url"] }
220+
.map do |cred|
221+
{
222+
"url" => cred.fetch("url").gsub(%r{/+$}, ""),
223+
"auth_headers" => auth_headers(cred.fetch("url").gsub(%r{/+$}, ""))
224+
}
225+
end
226+
end
227+
228+
sig { returns(T.class_of(Dependabot::Version)) }
229+
def version_class
230+
dependency.version_class
231+
end
232+
233+
sig { returns(T::Array[String]) }
234+
def central_repo_urls
235+
central_url_without_protocol = repository_finder.central_repo_url.gsub(%r{^.*://}, "")
236+
237+
%w(http:// https://).map { |p| p + central_url_without_protocol }
238+
end
239+
240+
sig { returns(Utils::AuthHeadersFinder) }
241+
def auth_headers_finder
242+
return @auth_headers_finder if @auth_headers_finder
243+
244+
@auth_headers_finder = Utils::AuthHeadersFinder.new(credentials)
245+
@auth_headers_finder
246+
end
247+
248+
sig { params(maven_repo_url: String).returns(T::Hash[String, String]) }
249+
def auth_headers(maven_repo_url)
250+
auth_headers_finder.auth_headers(maven_repo_url)
251+
end
252+
end
253+
end
254+
end
255+
end

0 commit comments

Comments
 (0)