Skip to content

Instantly share code, notes, and snippets.

@yb66
Last active February 20, 2021 04:33
Show Gist options
  • Save yb66/578d3a84a4213ba0724c3f5b71a2969d to your computer and use it in GitHub Desktop.
Save yb66/578d3a84a4213ba0724c3f5b71a2969d to your computer and use it in GitHub Desktop.
Refresh/check cacert once every week to keep curl updated
<?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>
#!/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