2024-08-12 10:30:59 +01:00
|
|
|
# typed: true # rubocop:todo Sorbet/StrictSigil
|
2021-08-06 02:30:44 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "api/analytics"
|
|
|
|
require "api/cask"
|
|
|
|
require "api/formula"
|
2024-05-17 14:42:44 +09:00
|
|
|
require "warnings"
|
|
|
|
Warnings.ignore :default_gems do
|
|
|
|
require "base64" # TODO: Add this to the Gemfile or remove it before moving to Ruby 3.4.
|
|
|
|
end
|
2021-08-06 02:30:44 -04:00
|
|
|
|
|
|
|
module Homebrew
|
|
|
|
# Helper functions for using Homebrew's formulae.brew.sh API.
|
|
|
|
module API
|
2021-08-09 10:29:55 -04:00
|
|
|
extend Cachable
|
|
|
|
|
2021-12-07 00:13:56 +00:00
|
|
|
HOMEBREW_CACHE_API = (HOMEBREW_CACHE/"api").freeze
|
2023-04-18 00:22:13 +01:00
|
|
|
HOMEBREW_CACHE_API_SOURCE = (HOMEBREW_CACHE/"api-source").freeze
|
2023-02-03 10:22:50 +00:00
|
|
|
|
2023-01-26 17:36:40 +00:00
|
|
|
sig { params(endpoint: String).returns(Hash) }
|
2023-02-11 22:16:57 -08:00
|
|
|
def self.fetch(endpoint)
|
2021-08-09 10:29:55 -04:00
|
|
|
return cache[endpoint] if cache.present? && cache.key?(endpoint)
|
2021-08-06 02:30:44 -04:00
|
|
|
|
2023-02-03 14:10:40 +00:00
|
|
|
api_url = "#{Homebrew::EnvConfig.api_domain}/#{endpoint}"
|
2023-02-03 10:22:50 +00:00
|
|
|
output = Utils::Curl.curl_output("--fail", api_url)
|
2023-02-03 14:10:40 +00:00
|
|
|
if !output.success? && Homebrew::EnvConfig.api_domain != HOMEBREW_API_DEFAULT_DOMAIN
|
|
|
|
# Fall back to the default API domain and try again
|
|
|
|
api_url = "#{HOMEBREW_API_DEFAULT_DOMAIN}/#{endpoint}"
|
|
|
|
output = Utils::Curl.curl_output("--fail", api_url)
|
|
|
|
end
|
2021-08-06 02:30:44 -04:00
|
|
|
raise ArgumentError, "No file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success?
|
|
|
|
|
2024-01-09 18:07:08 +00:00
|
|
|
cache[endpoint] = JSON.parse(output.stdout, freeze: true)
|
2021-08-06 02:30:44 -04:00
|
|
|
rescue JSON::ParserError
|
|
|
|
raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}"
|
|
|
|
end
|
2022-12-30 01:01:52 -05:00
|
|
|
|
2023-07-05 17:18:12 +01:00
|
|
|
sig {
|
|
|
|
params(endpoint: String, target: Pathname, stale_seconds: Integer).returns([T.any(Array, Hash), T::Boolean])
|
|
|
|
}
|
|
|
|
def self.fetch_json_api_file(endpoint, target: HOMEBREW_CACHE_API/endpoint,
|
|
|
|
stale_seconds: Homebrew::EnvConfig.api_auto_update_secs.to_i)
|
2024-07-18 23:02:51 +02:00
|
|
|
# Lazy-load dependency.
|
|
|
|
require "development_tools"
|
|
|
|
|
2022-12-30 01:01:52 -05:00
|
|
|
retry_count = 0
|
2023-02-03 14:10:40 +00:00
|
|
|
url = "#{Homebrew::EnvConfig.api_domain}/#{endpoint}"
|
|
|
|
default_url = "#{HOMEBREW_API_DEFAULT_DOMAIN}/#{endpoint}"
|
2023-02-10 19:15:31 +00:00
|
|
|
|
2023-02-23 10:04:50 +00:00
|
|
|
if Homebrew.running_as_root_but_not_owned_by_root? &&
|
|
|
|
(!target.exist? || target.empty?)
|
2023-02-23 12:48:18 +00:00
|
|
|
odie "Need to download #{url} but cannot as root! Run `brew update` without `sudo` first then try again."
|
2023-02-23 10:04:50 +00:00
|
|
|
end
|
|
|
|
|
2023-09-03 09:10:25 -04:00
|
|
|
curl_args = Utils::Curl.curl_args(retries: 0) + %W[
|
2023-02-10 17:27:10 +00:00
|
|
|
--compressed
|
|
|
|
--speed-limit #{ENV.fetch("HOMEBREW_CURL_SPEED_LIMIT")}
|
|
|
|
--speed-time #{ENV.fetch("HOMEBREW_CURL_SPEED_TIME")}
|
|
|
|
]
|
2023-02-10 19:15:31 +00:00
|
|
|
|
2023-10-04 22:24:57 -04:00
|
|
|
insecure_download = DevelopmentTools.ca_file_substitution_required? ||
|
|
|
|
DevelopmentTools.curl_substitution_required?
|
2023-02-10 19:15:31 +00:00
|
|
|
skip_download = target.exist? &&
|
|
|
|
!target.empty? &&
|
2023-03-10 17:53:15 +00:00
|
|
|
(!Homebrew.auto_update_command? ||
|
|
|
|
Homebrew::EnvConfig.no_auto_update? ||
|
2023-07-05 17:18:12 +01:00
|
|
|
((Time.now - stale_seconds) < target.mtime))
|
2023-02-23 10:04:50 +00:00
|
|
|
skip_download ||= Homebrew.running_as_root_but_not_owned_by_root?
|
2023-02-03 10:22:50 +00:00
|
|
|
|
2023-02-19 00:54:45 +00:00
|
|
|
json_data = begin
|
2023-02-04 13:22:15 +01:00
|
|
|
begin
|
2023-02-09 18:26:37 +00:00
|
|
|
args = curl_args.dup
|
2023-02-11 22:16:57 -08:00
|
|
|
args.prepend("--time-cond", target.to_s) if target.exist? && !target.empty?
|
2023-08-21 21:48:36 -04:00
|
|
|
if insecure_download
|
2023-10-04 22:24:57 -04:00
|
|
|
opoo DevelopmentTools.insecure_download_warning(endpoint)
|
2023-08-21 21:48:36 -04:00
|
|
|
args.append("--insecure")
|
|
|
|
end
|
2023-02-10 19:15:31 +00:00
|
|
|
unless skip_download
|
2023-02-20 23:23:42 -08:00
|
|
|
ohai "Downloading #{url}" if $stdout.tty? && !Context.current.quiet?
|
2023-02-10 19:15:31 +00:00
|
|
|
# Disable retries here, we handle them ourselves below.
|
|
|
|
Utils::Curl.curl_download(*args, url, to: target, retries: 0, show_error: false)
|
|
|
|
end
|
2023-02-04 13:22:15 +01:00
|
|
|
rescue ErrorDuringExecution
|
2023-02-03 14:10:40 +00:00
|
|
|
if url == default_url
|
|
|
|
raise unless target.exist?
|
|
|
|
raise if target.empty?
|
|
|
|
elsif retry_count.zero? || !target.exist? || target.empty?
|
|
|
|
# Fall back to the default API domain and try again
|
|
|
|
# This block will be executed only once, because we set `url` to `default_url`
|
|
|
|
url = default_url
|
|
|
|
target.unlink if target.exist? && target.empty?
|
2023-02-10 19:15:31 +00:00
|
|
|
skip_download = false
|
2023-02-03 14:10:40 +00:00
|
|
|
|
|
|
|
retry
|
|
|
|
end
|
2022-12-30 01:01:52 -05:00
|
|
|
|
2023-02-04 13:22:15 +01:00
|
|
|
opoo "#{target.basename}: update failed, falling back to cached version."
|
|
|
|
end
|
2023-02-03 10:22:50 +00:00
|
|
|
|
2023-08-21 21:48:36 -04:00
|
|
|
mtime = insecure_download ? Time.new(1970, 1, 1) : Time.now
|
2024-03-07 16:20:20 +00:00
|
|
|
FileUtils.touch(target, mtime:) unless skip_download
|
2024-01-09 18:07:08 +00:00
|
|
|
JSON.parse(target.read, freeze: true)
|
2022-12-30 01:01:52 -05:00
|
|
|
rescue JSON::ParserError
|
|
|
|
target.unlink
|
|
|
|
retry_count += 1
|
2023-02-10 19:15:31 +00:00
|
|
|
skip_download = false
|
2023-02-03 10:22:50 +00:00
|
|
|
odie "Cannot download non-corrupt #{url}!" if retry_count > Homebrew::EnvConfig.curl_retries.to_i
|
2022-12-30 01:01:52 -05:00
|
|
|
|
|
|
|
retry
|
|
|
|
end
|
2023-02-19 00:54:45 +00:00
|
|
|
|
|
|
|
if endpoint.end_with?(".jws.json")
|
|
|
|
success, data = verify_and_parse_jws(json_data)
|
|
|
|
unless success
|
|
|
|
target.unlink
|
|
|
|
odie <<~EOS
|
|
|
|
Failed to verify integrity (#{data}) of:
|
|
|
|
#{url}
|
|
|
|
Potential MITM attempt detected. Please run `brew update` and try again.
|
|
|
|
EOS
|
|
|
|
end
|
|
|
|
[data, !skip_download]
|
|
|
|
else
|
|
|
|
[json_data, !skip_download]
|
|
|
|
end
|
2022-12-30 01:01:52 -05:00
|
|
|
end
|
2023-01-26 17:36:40 +00:00
|
|
|
|
2023-02-10 12:07:36 -05:00
|
|
|
sig { params(json: Hash).returns(Hash) }
|
2023-02-11 22:16:57 -08:00
|
|
|
def self.merge_variations(json)
|
2024-01-01 19:10:48 -08:00
|
|
|
return json unless json.key?("variations")
|
|
|
|
|
2023-03-02 23:29:09 -05:00
|
|
|
bottle_tag = ::Utils::Bottles::Tag.new(system: Homebrew::SimulateSystem.current_os,
|
|
|
|
arch: Homebrew::SimulateSystem.current_arch)
|
|
|
|
|
2024-01-01 19:10:48 -08:00
|
|
|
if (variation = json.dig("variations", bottle_tag.to_s).presence)
|
2023-02-10 12:07:36 -05:00
|
|
|
json = json.merge(variation)
|
|
|
|
end
|
|
|
|
|
|
|
|
json.except("variations")
|
|
|
|
end
|
2023-02-16 21:49:03 +00:00
|
|
|
|
|
|
|
sig { params(names: T::Array[String], type: String, regenerate: T::Boolean).returns(T::Boolean) }
|
|
|
|
def self.write_names_file(names, type, regenerate:)
|
|
|
|
names_path = HOMEBREW_CACHE_API/"#{type}_names.txt"
|
|
|
|
if !names_path.exist? || regenerate
|
|
|
|
names_path.write(names.join("\n"))
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
false
|
|
|
|
end
|
2023-02-19 00:54:45 +00:00
|
|
|
|
|
|
|
sig { params(json_data: Hash).returns([T::Boolean, T.any(String, Array, Hash)]) }
|
|
|
|
private_class_method def self.verify_and_parse_jws(json_data)
|
|
|
|
signatures = json_data["signatures"]
|
|
|
|
homebrew_signature = signatures&.find { |sig| sig.dig("header", "kid") == "homebrew-1" }
|
|
|
|
return false, "key not found" if homebrew_signature.nil?
|
|
|
|
|
|
|
|
header = JSON.parse(Base64.urlsafe_decode64(homebrew_signature["protected"]))
|
|
|
|
if header["alg"] != "PS512" || header["b64"] != false # NOTE: nil has a meaning of true
|
|
|
|
return false, "invalid algorithm"
|
|
|
|
end
|
|
|
|
|
|
|
|
require "openssl"
|
|
|
|
|
|
|
|
pubkey = OpenSSL::PKey::RSA.new((HOMEBREW_LIBRARY_PATH/"api/homebrew-1.pem").read)
|
|
|
|
signing_input = "#{homebrew_signature["protected"]}.#{json_data["payload"]}"
|
|
|
|
unless pubkey.verify_pss("SHA512",
|
|
|
|
Base64.urlsafe_decode64(homebrew_signature["signature"]),
|
|
|
|
signing_input,
|
|
|
|
salt_length: :digest,
|
|
|
|
mgf1_hash: "SHA512")
|
|
|
|
return false, "signature mismatch"
|
|
|
|
end
|
|
|
|
|
2024-01-09 18:07:08 +00:00
|
|
|
[true, JSON.parse(json_data["payload"], freeze: true)]
|
2023-02-19 00:54:45 +00:00
|
|
|
end
|
2023-04-18 00:22:13 +01:00
|
|
|
|
|
|
|
sig { params(path: Pathname).returns(T.nilable(Tap)) }
|
|
|
|
def self.tap_from_source_download(path)
|
2024-02-16 23:19:39 +01:00
|
|
|
path = path.expand_path
|
2023-04-18 00:22:13 +01:00
|
|
|
source_relative_path = path.relative_path_from(Homebrew::API::HOMEBREW_CACHE_API_SOURCE)
|
|
|
|
return if source_relative_path.to_s.start_with?("../")
|
|
|
|
|
|
|
|
org, repo = source_relative_path.each_filename.first(2)
|
|
|
|
return if org.blank? || repo.blank?
|
|
|
|
|
|
|
|
Tap.fetch(org, repo)
|
|
|
|
end
|
2024-02-10 22:24:36 -08:00
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def self.internal_json_v3?
|
2024-02-18 19:28:44 -08:00
|
|
|
ENV["HOMEBREW_INTERNAL_JSON_V3"].present?
|
2024-02-10 22:24:36 -08:00
|
|
|
end
|
2021-08-06 02:30:44 -04:00
|
|
|
end
|
2023-06-19 03:57:52 +01:00
|
|
|
|
|
|
|
sig { params(block: T.proc.returns(T.untyped)).returns(T.untyped) }
|
|
|
|
def self.with_no_api_env(&block)
|
|
|
|
return yield if Homebrew::EnvConfig.no_install_from_api?
|
|
|
|
|
|
|
|
with_env(HOMEBREW_NO_INSTALL_FROM_API: "1", HOMEBREW_AUTOMATICALLY_SET_NO_INSTALL_FROM_API: "1", &block)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { params(condition: T::Boolean, block: T.proc.returns(T.untyped)).returns(T.untyped) }
|
|
|
|
def self.with_no_api_env_if_needed(condition, &block)
|
|
|
|
return yield unless condition
|
|
|
|
|
|
|
|
with_no_api_env(&block)
|
|
|
|
end
|
2021-08-06 02:30:44 -04:00
|
|
|
end
|