
RFC 2616 states: A client MUST be prepared to accept one or more 1xx status responses prior to a regular response, even if the client does not expect a 100 (Continue) status message. Unexpected 1xx status responses MAY be ignored by a user agent. In the rare cases that we encounter a formula URL with a server that provides a preliminary 1xx status code, it seems that (at least during audit) we are failing on encountering this status code, even though retrieving the file will succeed without issues.
195 lines
6.1 KiB
Ruby
195 lines
6.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "open3"
|
|
|
|
def curl_executable
|
|
@curl ||= [
|
|
ENV["HOMEBREW_CURL"],
|
|
which("curl"),
|
|
"/usr/bin/curl",
|
|
].compact.map { |c| Pathname(c) }.find(&:executable?)
|
|
raise "no executable curl was found" unless @curl
|
|
|
|
@curl
|
|
end
|
|
|
|
def curl_args(*extra_args, show_output: false, user_agent: :default)
|
|
args = []
|
|
|
|
# do not load .curlrc unless requested (must be the first argument)
|
|
args << "-q" unless ENV["HOMEBREW_CURLRC"]
|
|
|
|
args << "--show-error"
|
|
|
|
args << "--user-agent" << case user_agent
|
|
when :browser, :fake
|
|
HOMEBREW_USER_AGENT_FAKE_SAFARI
|
|
when :default
|
|
HOMEBREW_USER_AGENT_CURL
|
|
else
|
|
user_agent
|
|
end
|
|
|
|
unless show_output
|
|
args << "--fail"
|
|
args << "--progress-bar" unless ARGV.verbose?
|
|
args << "--verbose" if ENV["HOMEBREW_CURL_VERBOSE"]
|
|
args << "--silent" unless $stdout.tty?
|
|
end
|
|
|
|
args << "--retry" << ENV["HOMEBREW_CURL_RETRIES"] if ENV["HOMEBREW_CURL_RETRIES"]
|
|
|
|
args + extra_args
|
|
end
|
|
|
|
def curl(*args, secrets: [], **options)
|
|
# SSL_CERT_FILE can be incorrectly set by users or portable-ruby and screw
|
|
# with SSL downloads so unset it here.
|
|
system_command! curl_executable,
|
|
args: curl_args(*args, **options),
|
|
print_stdout: true,
|
|
env: { "SSL_CERT_FILE" => nil },
|
|
secrets: secrets
|
|
end
|
|
|
|
def curl_download(*args, to: nil, **options)
|
|
destination = Pathname(to)
|
|
destination.dirname.mkpath
|
|
|
|
range_stdout = curl_output("--location", "--range", "0-1",
|
|
"--dump-header", "-",
|
|
"--write-out", "%{http_code}",
|
|
"--output", "/dev/null", *args, **options).stdout
|
|
headers, _, http_status = range_stdout.partition("\r\n\r\n")
|
|
|
|
supports_partial_download = http_status.to_i == 206 # Partial Content
|
|
if supports_partial_download &&
|
|
destination.exist? &&
|
|
destination.size == %r{^.*Content-Range: bytes \d+-\d+/(\d+)\r\n.*$}m.match(headers)&.[](1)&.to_i
|
|
return # We've already downloaded all the bytes
|
|
end
|
|
|
|
continue_at = if destination.exist? && supports_partial_download
|
|
"-"
|
|
else
|
|
0
|
|
end
|
|
|
|
curl("--location", "--remote-time", "--continue-at", continue_at.to_s, "--output", destination, *args, **options)
|
|
end
|
|
|
|
def curl_output(*args, secrets: [], **options)
|
|
system_command curl_executable,
|
|
args: curl_args(*args, show_output: true, **options),
|
|
print_stderr: false,
|
|
secrets: secrets
|
|
end
|
|
|
|
def curl_check_http_content(url, user_agents: [:default], check_content: false, strict: false)
|
|
return unless url.start_with? "http"
|
|
|
|
details = nil
|
|
user_agent = nil
|
|
hash_needed = url.start_with?("http:")
|
|
user_agents.each do |ua|
|
|
details = curl_http_content_headers_and_checksum(url, hash_needed: hash_needed, user_agent: ua)
|
|
user_agent = ua
|
|
break if (100..299).include?(details[:status].to_i)
|
|
end
|
|
|
|
unless details[:status]
|
|
# Hack around https://github.com/Homebrew/brew/issues/3199
|
|
return if MacOS.version == :el_capitan
|
|
|
|
return "The URL #{url} is not reachable"
|
|
end
|
|
|
|
unless (100..299).include?(details[:status].to_i)
|
|
return "The URL #{url} is not reachable (HTTP status code #{details[:status]})"
|
|
end
|
|
|
|
if url.start_with?("https://") && ENV["HOMEBREW_NO_INSECURE_REDIRECT"] &&
|
|
!details[:final_url].start_with?("https://")
|
|
return "The URL #{url} redirects back to HTTP"
|
|
end
|
|
|
|
return unless hash_needed
|
|
|
|
secure_url = url.sub "http", "https"
|
|
secure_details =
|
|
curl_http_content_headers_and_checksum(secure_url, hash_needed: true, user_agent: user_agent)
|
|
|
|
if !(100..299).include?(details[:status].to_i) ||
|
|
!(100..299).include?(secure_details[:status].to_i)
|
|
return
|
|
end
|
|
|
|
etag_match = details[:etag] &&
|
|
details[:etag] == secure_details[:etag]
|
|
content_length_match =
|
|
details[:content_length] &&
|
|
details[:content_length] == secure_details[:content_length]
|
|
file_match = details[:file_hash] == secure_details[:file_hash]
|
|
|
|
if (etag_match || content_length_match || file_match) &&
|
|
secure_details[:final_url].start_with?("https://") &&
|
|
url.start_with?("http://")
|
|
return "The URL #{url} should use HTTPS rather than HTTP"
|
|
end
|
|
|
|
return unless check_content
|
|
|
|
no_protocol_file_contents = %r{https?:\\?/\\?/}
|
|
details[:file] = details[:file].gsub(no_protocol_file_contents, "/")
|
|
secure_details[:file] = secure_details[:file].gsub(no_protocol_file_contents, "/")
|
|
|
|
# Check for the same content after removing all protocols
|
|
if (details[:file] == secure_details[:file]) &&
|
|
secure_details[:final_url].start_with?("https://") &&
|
|
url.start_with?("http://")
|
|
return "The URL #{url} should use HTTPS rather than HTTP"
|
|
end
|
|
|
|
return unless strict
|
|
|
|
# Same size, different content after normalization
|
|
# (typical causes: Generated ID, Timestamp, Unix time)
|
|
if details[:file].length == secure_details[:file].length
|
|
return "The URL #{url} may be able to use HTTPS rather than HTTP. Please verify it in a browser."
|
|
end
|
|
|
|
lenratio = (100 * secure_details[:file].length / details[:file].length).to_i
|
|
return unless (90..110).cover?(lenratio)
|
|
|
|
"The URL #{url} may be able to use HTTPS rather than HTTP. Please verify it in a browser."
|
|
end
|
|
|
|
def curl_http_content_headers_and_checksum(url, hash_needed: false, user_agent: :default)
|
|
max_time = hash_needed ? "600" : "25"
|
|
output, = curl_output(
|
|
"--connect-timeout", "15", "--include", "--max-time", max_time, "--location", url,
|
|
user_agent: user_agent
|
|
)
|
|
|
|
status_code = :unknown
|
|
while status_code == :unknown || status_code.to_s.start_with?("3")
|
|
headers, _, output = output.partition("\r\n\r\n")
|
|
status_code = headers[%r{HTTP\/.* (\d+)}, 1]
|
|
final_url = headers[/^Location:\s*(.*)$/i, 1]&.chomp
|
|
end
|
|
|
|
output_hash = Digest::SHA256.digest(output) if hash_needed
|
|
|
|
final_url ||= url
|
|
|
|
{
|
|
url: url,
|
|
final_url: final_url,
|
|
status: status_code,
|
|
etag: headers[%r{ETag: ([wW]\/)?"(([^"]|\\")*)"}, 2],
|
|
content_length: headers[/Content-Length: (\d+)/, 1],
|
|
file_hash: output_hash,
|
|
file: output,
|
|
}
|
|
end
|