Last active
February 20, 2021 04:33
-
-
Save yb66/578d3a84a4213ba0724c3f5b71a2969d to your computer and use it in GitHub Desktop.
Refresh/check cacert once every week to keep curl updated
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<!-- Run the cacert refresh once a week --> | |
<!-- ~/Library/LaunchAgents/cacert-refresh.plist --> | |
<plist version="1.0"> | |
<dict> | |
<key>Label</key> | |
<string>cacert-refresh</string> | |
<key>EnableGlobbing</key> | |
<true/> | |
<key>ProgramArguments</key> | |
<array> | |
<string>~/Library/Application Support/CAcert-Refresh/cacert-refresh.rb</string> | |
</array> | |
<key>EnvironmentVariables</key> | |
<dict> | |
<key>CA_CERT_FILE</key> | |
<string>~/Library/Frameworks/OpenSSL.framework/ssl/certs/cacert.pem</string> | |
<key>SSL_CERT_FILE</key> | |
<string>~/Library/Frameworks/OpenSSL.framework/ssl/certs/cacert.pem</string> | |
<key>OPENSSL_DIR</key> | |
<string>~/Library/Frameworks/OpenSSL.framework/Versions/Current</string> | |
</dict> | |
<key>StartCalendarInterval</key> | |
<dict> | |
<key>Weekday</key> | |
<integer>1</integer> | |
</dict> | |
<key>RunAtLoad</key> | |
<true/> | |
<key>ProcessType</key> | |
<string>Background</string> | |
<key>StandardOutPath</key> | |
<string>~/Library/Logs/CAcert-Refresh/stdout.log</string> | |
<key>StandardErrorPath</key> | |
<string>~/Library/Logs/CAcert-Refresh/stderr.log</string> | |
</dict> | |
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# A script to refresh the cacert with one from the haxx's curl site. | |
require 'pathname' | |
require 'net/http' | |
require 'uri' | |
require 'tmpdir' | |
require 'digest/sha2' | |
require 'fileutils' | |
require 'time' | |
module CAcert | |
# The gist of things # | |
# | |
# # Haxx has a file | |
# / \ | |
## I have a file # I have no file | |
# | | | |
## Get mtime on my file | | |
# | | | |
## Get mtime on his file | | |
# / \ | | |
## No diff # If differ --- # create download command | |
# | | | |
# | # download file | |
# | | | |
# | # download cert | |
# | | | |
# | # If bad <---- # check sha | |
# | / | | |
# | / # If good | |
# | / | | |
# | / # move cacert to ssl dir | |
# | / | | |
# | / # set mtime on file | |
# | / | |
# | / | |
# exit and write to log | |
class Refresh | |
class Logger < ::Hash | |
def initialize | |
super | |
@logs = [] | |
@errors = [] | |
end | |
def add_to_log message | |
t = Time.now | |
self.store t, message | |
@logs << t | |
end | |
def add_to_error_log message | |
t = Time.now | |
self.store t, message | |
@errors << t | |
end | |
def flush_logs | |
@logs.each do |t| | |
STDOUT.puts "#{t.rfc2822}: #{self.delete t}" | |
end | |
@logs.clear | |
end | |
def flush_errors | |
@errors.each do |t| | |
STDERR.puts "#{t.rfc2822}: #{self.delete t}" | |
end | |
@errors.clear | |
end | |
end | |
module ConnectionError; end | |
class Error < StandardError; end | |
class << self | |
def last_modified cert_file_url | |
uri = URI.parse(cert_file_url) | |
https = Net::HTTP.new(uri.host, uri.port) | |
https.use_ssl = true | |
res = https.head '/' | |
fail ConnectionError, "Status code: #{res.code}" unless res.code.to_s.start_with? "2" | |
::Time.parse(res['last-modified']) | |
end | |
def mtimes_differ? cert_file, last_modified | |
end | |
def download uri, target | |
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| | |
http.request Net::HTTP::Get.new(uri) do |response| | |
open target, 'w' do |io| | |
response.read_body do |chunk| | |
io.write chunk | |
end | |
end | |
end | |
end | |
rescue HTTPClientError => e | |
e.extend CAcert::Refresh::ConnectionError | |
raise | |
rescue HTTPServerError => e | |
e.extend CAcert::Refresh::ConnectionError | |
raise | |
end | |
def flush_logs runner | |
runner.logger.flush_logs | |
runner.logger.flush_errors | |
end | |
end | |
HAXX_SITE_DEFAULT = "https://curl.se/ca" | |
CERT_FILE_DEFAULT = "cacert.pem" | |
def initialize(haxx_site: HAXX_SITE_DEFAULT, cert_file: CERT_FILE_DEFAULT) | |
@logger = CAcert::Refresh::Logger.new | |
@logger.add_to_log "--- Session begins --- " | |
@return_code = 0 | |
@frozen = false | |
@haxx_site = haxx_site | |
@cert_file = cert_file | |
@cert_file_url = "#{@haxx_site}/#{@cert_file}" | |
@shacert = "#{@cert_file}.sha256" | |
@sha_url = "#{@haxx_site}/#{@shacert}" | |
if ENV["CA_CERT_FILE"] | |
@cert_file_path = Pathname(ENV["CA_CERT_FILE"]) | |
@certs_dir = @cert_file_path.parent | |
elsif ENV["SSL_CERT_FILE"] | |
@cert_file_path = Pathname(ENV["SSL_CERT_FILE"]) | |
@certs_dir = @cert_file_path.parent | |
elsif ENV["OPENSSL_DIR"] | |
@certs_dir = Pathname(ENV["OPENSSL_DIR"]).join("ssl/certs") | |
@cert_file_path = @certs_dir.join(@cert_file) | |
end | |
end | |
attr_reader :return_code, :logger | |
def frozen? | |
@frozen | |
end | |
def return_code= code | |
return if frozen? | |
@frozen = true | |
@return_code = code | |
end | |
def mtimes_differ? | |
@last_modified ||= self.class.last_modified @cert_file_url | |
@mtime = File.mtime(@cert_file_path) | |
@last_modified == @mtime | |
end | |
def sha256_match? | |
# check the sha sum | |
@sha = Digest::SHA256.hexdigest File.read cert_file | |
@expected_sha = File.read(shacert).split(/\s+/).first | |
@expected_sha == @sha | |
end | |
def download_cert_file | |
self.class.download URI(@cert_file_url), @cert_file | |
end | |
def download_sha_cert | |
self.class.download URI(@sha_url), @shacert | |
end | |
def run! | |
fail if frozen? | |
if @cert_file_path.exist? and not mtimes_differ? | |
@logger.add_to_log "No difference in mtimes. Exiting." | |
self.return_code = 0 | |
return | |
end | |
Dir.mktmpdir do |tmpdir| | |
download_cert_file | |
download_sha_cert | |
if sha256_match? | |
if @cert_file_path.exist? | |
::FileUtils.mv @cert_file_path, "#{@cert_file_path.realpath.to_s}.old" | |
end | |
::FileUtils.mv @cert_file, @certs_dir | |
::FileUtils.touch @cert_file_path, mtime: @last_modified | |
@logger.add_to_log "New certs file installed with hash " | |
else | |
# It's not an error but it's not expected behaviour either | |
# so log to STDOUT but return a failure code | |
@logger.add_to_log "The shas do not match:" | |
@logger.add_to_log "cert sha = #{@sha}" | |
@logger.add_to_log "expected sha = #{@expected_sha}" | |
self.return_code = 1 | |
end | |
end | |
rescue CAcert::Refresh::ConnectionError => e | |
@logger.add_to_error_log "#{Time.now.rfc2822}: #{e.message}" | |
self.return_code = 1 | |
return | |
rescue CAcert::Refresh::Error => e | |
@logger.add_to_error_log "#{Time.now.rfc2822}: #{e.message}" | |
self.return_code = 1 | |
return | |
rescue => e | |
@logger.add_to_error_log "#{Time.now.rfc2822}: #{e.message}" | |
return_code = 1 | |
ensure | |
CAcert::Refresh.flush_logs self | |
end | |
end | |
end | |
runner = CAcert::Refresh.new | |
runner.run! | |
exit runner.return_code |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment