| 
									
										
										
										
											2023-02-11 22:16:57 -08:00
										 |  |  | # typed: true | 
					
						
							| 
									
										
										
										
											2021-08-06 02:30:44 -04:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "api/analytics" | 
					
						
							|  |  |  | require "api/cask" | 
					
						
							|  |  |  | require "api/formula" | 
					
						
							| 
									
										
										
										
											2021-08-09 10:29:55 -04:00
										 |  |  | require "extend/cachable" | 
					
						
							| 
									
										
										
										
											2021-08-06 02:30:44 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | module Homebrew | 
					
						
							|  |  |  |   # Helper functions for using Homebrew's formulae.brew.sh API. | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # @api private | 
					
						
							|  |  |  |   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? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 17:36:40 +00:00
										 |  |  |       cache[endpoint] = JSON.parse(output.stdout) | 
					
						
							| 
									
										
										
										
											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-02-16 21:49:03 +00:00
										 |  |  |     sig { params(endpoint: String, target: Pathname).returns([T.any(Array, Hash), T::Boolean]) } | 
					
						
							| 
									
										
										
										
											2023-02-11 22:16:57 -08:00
										 |  |  |     def self.fetch_json_api_file(endpoint, target:) | 
					
						
							| 
									
										
										
										
											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-02-10 19:15:31 +00:00
										 |  |  |       # TODO: consider using more of Utils::Curl | 
					
						
							| 
									
										
										
										
											2023-02-10 17:27:10 +00:00
										 |  |  |       curl_args = %W[
 | 
					
						
							|  |  |  |         --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
										 |  |  |       curl_args << "--progress-bar" unless Context.current.verbose? | 
					
						
							|  |  |  |       curl_args << "--verbose" if Homebrew::EnvConfig.curl_verbose? | 
					
						
							| 
									
										
										
										
											2023-02-20 23:23:42 -08:00
										 |  |  |       curl_args << "--silent" if !$stdout.tty? || Context.current.quiet? | 
					
						
							| 
									
										
										
										
											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-02-10 19:15:31 +00:00
										 |  |  |                       ((Time.now - Homebrew::EnvConfig.api_auto_update_secs.to_i) < 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-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-02-16 15:41:26 +00:00
										 |  |  |         FileUtils.touch(target) unless skip_download | 
					
						
							| 
									
										
										
										
											2023-02-19 00:54:45 +00:00
										 |  |  |         JSON.parse(target.read) | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							| 
									
										
										
										
											2023-03-02 23:29:09 -05:00
										 |  |  |       bottle_tag = ::Utils::Bottles::Tag.new(system: Homebrew::SimulateSystem.current_os, | 
					
						
							|  |  |  |                                              arch:   Homebrew::SimulateSystem.current_arch) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (variations = json["variations"].presence) && | 
					
						
							| 
									
										
										
										
											2023-03-07 14:12:03 -05:00
										 |  |  |          (variation = 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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       [true, JSON.parse(json_data["payload"])] | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2023-04-18 00:22:13 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     sig { params(path: Pathname).returns(T.nilable(Tap)) } | 
					
						
							|  |  |  |     def self.tap_from_source_download(path) | 
					
						
							|  |  |  |       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 | 
					
						
							| 
									
										
										
										
											2021-08-06 02:30:44 -04:00
										 |  |  |   end | 
					
						
							|  |  |  | end |