diff options
Diffstat (limited to 'lib/leap_cli/commands/ca.rb')
-rw-r--r-- | lib/leap_cli/commands/ca.rb | 649 |
1 files changed, 237 insertions, 412 deletions
diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb index 1b311eee..3c5fc7d5 100644 --- a/lib/leap_cli/commands/ca.rb +++ b/lib/leap_cli/commands/ca.rb @@ -1,8 +1,3 @@ -autoload :OpenSSL, 'openssl' -autoload :CertificateAuthority, 'certificate_authority' -autoload :Date, 'date' -require 'digest/md5' - module LeapCli; module Commands desc "Manage X.509 certificates" @@ -35,41 +30,15 @@ module LeapCli; module Commands cert.desc 'Creates a Diffie-Hellman parameter file, needed for forward secret OpenVPN ciphers.' # (needed for server-side of some TLS connections) cert.command :dh do |dh| dh.action do |global_options,options,args| - long_running do - if cmd_exists?('certtool') - log 0, 'Generating DH parameters (takes a long time)...' - output = assert_run!('certtool --generate-dh-params --sec-param high') - output.sub! /.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1' - output << "\n" - write_file!(:dh_params, output) - else - log 0, 'Generating DH parameters (takes a REALLY long time)...' - output = OpenSSL::PKey::DH.generate(3248).to_pem - write_file!(:dh_params, output) - end - end + generate_dh end end - # - # hints: - # - # inspect CSR: - # openssl req -noout -text -in files/cert/x.csr - # - # generate CSR with openssl to see how it compares: - # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr - # - # validate a CSR: - # http://certlogik.com/decoder/ - # - # nice details about CSRs: - # http://www.redkestrel.co.uk/Articles/CSR.html - # cert.desc "Creates a CSR for use in buying a commercial X.509 certificate." cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. "+ "The properties used for this CSR come from `provider.ca.server_certificates`, "+ "but may be overridden here." + cert.arg_name "DOMAIN" cert.command :csr do |csr| csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.' csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name." @@ -81,70 +50,26 @@ module LeapCli; module Commands csr.flag :bits, :arg_name => 'BITS', :desc => "Override default certificate bit length" csr.flag :digest, :arg_name => 'DIGEST', :desc => "Override default signature digest" csr.action do |global_options,options,args| - assert_config! 'provider.domain' - assert_config! 'provider.name' - assert_config! 'provider.default_language' - assert_config! 'provider.ca.server_certificates.bit_size' - assert_config! 'provider.ca.server_certificates.digest' - domain = options[:domain] || provider.domain - - unless global_options[:force] - assert_files_missing! [:commercial_key, domain], [:commercial_csr, domain], - :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.' - end - - server_certificates = provider.ca.server_certificates - - # RSA key - keypair = CertificateAuthority::MemoryKeyMaterial.new - bit_size = (options[:bits] || server_certificates.bit_size).to_i - log :generating, "%s bit RSA key" % bit_size do - keypair.generate_key(bit_size) - write_file! [:commercial_key, domain], keypair.private_key.to_pem - end - - # CSR - dn = CertificateAuthority::DistinguishedName.new - dn.common_name = domain - dn.organization = options[:organization] || provider.name[provider.default_language] - dn.ou = options[:organizational_unit] # optional - dn.email_address = options[:email] # optional - dn.country = options[:country] || server_certificates['country'] # optional - dn.state = options[:state] || server_certificates['state'] # optional - dn.locality = options[:locality] || server_certificates['locality'] # optional - - digest = options[:digest] || server_certificates.digest - log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do - csr = create_csr(dn, keypair, digest) - request = csr.to_x509_csr - write_file! [:commercial_csr, domain], csr.to_pem - end + generate_csr(global_options, options, args) + end + end - # Sign using our own CA, for use in testing but hopefully not production. - # It is not that commerical CAs are so secure, it is just that signing your own certs is - # a total drag for the user because they must click through dire warnings. - #if options[:sign] - log :generating, "self-signed x509 server certificate for testing purposes" do - cert = csr.to_cert - cert.serial_number.number = cert_serial_number(domain) - cert.not_before = yesterday - cert.not_after = yesterday.advance(:years => 1) - cert.parent = ca_root - cert.sign! domain_test_signing_profile - write_file! [:commercial_cert, domain], cert.to_pem - log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}" - end - #end + cert.desc "Register an authorization key with the CA letsencrypt.org" + cert.long_desc "This only needs to be done once." + cert.command :register do |register| + register.action do |global, options, args| + do_register_key(global, options, args) + end + end - # FAKE CA - unless file_exists? :commercial_ca_cert - log :using, "generated CA in place of commercial CA for testing purposes" do - write_file! :commercial_ca_cert, read_file!(:ca_cert) - log "please also replace this file with the CA cert from the commercial authority you use." - end - end + cert.desc "Renews a certificate using the CA letsencrypt.org" + cert.arg_name "DOMAIN" + cert.command :renew do |renew| + renew.action do |global, options, args| + do_renew_cert(global, options, args) end end + end protected @@ -153,6 +78,7 @@ module LeapCli; module Commands # will generate new certificates for the specified nodes, if needed. # def update_certificates(nodes, options={}) + require 'leap_cli/x509' assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them' assert_config! 'provider.ca.server_certificates.bit_size' assert_config! 'provider.ca.server_certificates.digest' @@ -160,382 +86,281 @@ module LeapCli; module Commands assert_config! 'common.x509.use' nodes.each_node do |node| - warn_if_commercial_cert_will_soon_expire(node) + node.warn_if_commercial_cert_will_soon_expire if !node.x509.use remove_file!([:node_x509_key, node.name]) remove_file!([:node_x509_cert, node.name]) - elsif options[:force] || cert_needs_updating?(node) - generate_cert_for_node(node) + elsif options[:force] || node.cert_needs_updating? + node.generate_cert end end end + # + # yields client key and cert suitable for testing + # + def generate_test_client_cert(prefix=nil) + require 'leap_cli/x509' + cert = CertificateAuthority::Certificate.new + cert.serial_number.number = cert_serial_number(provider.domain) + cert.subject.common_name = [prefix, random_common_name(provider.domain)].join + cert.not_before = X509.yesterday + cert.not_after = X509.yesterday.advance(:years => 1) + cert.key_material.generate_key(1024) # just for testing, remember! + cert.parent = client_ca_root + cert.sign! client_test_signing_profile + yield cert.key_material.private_key.to_pem, cert.to_pem + end + private def generate_new_certificate_authority(key_file, cert_file, common_name) + require 'leap_cli/x509' assert_files_missing! key_file, cert_file assert_config! 'provider.ca.name' assert_config! 'provider.ca.bit_size' assert_config! 'provider.ca.life_span' - root = CertificateAuthority::Certificate.new + root = X509.new_ca(provider.ca, common_name) - # set subject - root.subject.common_name = common_name - possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address'] - provider.ca.keys.each do |key| - if possible.include?(key) - root.subject.send(key + '=', provider.ca[key]) + write_file!(key_file, root.key_material.private_key.to_pem) + write_file!(cert_file, root.to_pem) + end + + def generate_dh + require 'leap_cli/x509' + long_running do + if cmd_exists?('certtool') + log 0, 'Generating DH parameters (takes a long time)...' + output = assert_run!('certtool --generate-dh-params --sec-param high') + output.sub!(/.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1') + output << "\n" + write_file!(:dh_params, output) + else + log 0, 'Generating DH parameters (takes a REALLY long time)...' + output = OpenSSL::PKey::DH.generate(3248).to_pem + write_file!(:dh_params, output) end end + end - # set expiration - root.not_before = yesterday - root.not_after = yesterday_advance(provider.ca.life_span) - - # generate private key - root.serial_number.number = 1 - root.key_material.generate_key(provider.ca.bit_size) + # + # hints: + # + # inspect CSR: + # openssl req -noout -text -in files/cert/x.csr + # + # generate CSR with openssl to see how it compares: + # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr + # + # validate a CSR: + # http://certlogik.com/decoder/ + # + # nice details about CSRs: + # http://www.redkestrel.co.uk/Articles/CSR.html + # + def generate_csr(global_options, options, args) + require 'leap_cli/x509' + assert_config! 'provider.domain' + assert_config! 'provider.name' + assert_config! 'provider.default_language' + assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.digest' - # sign self - root.signing_entity = true - root.parent = root - root.sign!(ca_root_signing_profile) + server_certificates = provider.ca.server_certificates + options[:domain] ||= args.first || provider.domain + options[:organization] ||= provider.name[provider.default_language] + options[:country] ||= server_certificates['country'] + options[:state] ||= server_certificates['state'] + options[:locality] ||= server_certificates['locality'] + options[:bits] ||= server_certificates.bit_size + options[:digest] ||= server_certificates.digest + + unless global_options[:force] + assert_files_missing! [:commercial_key, options[:domain]], [:commercial_csr, options[:domain]], + :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.' + end - # save - write_file!(key_file, root.key_material.private_key.to_pem) - write_file!(cert_file, root.to_pem) + X509.create_csr_and_cert(options) end # - # returns true if the certs associated with +node+ need to be regenerated. + # letsencrypt.org # - def cert_needs_updating?(node) - if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name]) - return true - else - cert = load_certificate_file([:node_x509_cert, node.name]) - if !created_by_authority?(cert, ca_root) - log :updating, "cert for node '#{node.name}' because it was signed by an old CA root cert." - return true - end - if cert.not_after < Time.now.advance(:months => 2) - log :updating, "cert for node '#{node.name}' because it will expire soon" - return true - end - if cert.subject.common_name != node.domain.full - log :updating, "cert for node '#{node.name}' because domain.full has changed (was #{cert.subject.common_name}, now #{node.domain.full})" - return true + + def do_register_key(global, options, args) + require 'leap_cli/acme' + assert_config! 'provider.contacts.default' + contact = manager.provider.contacts.default.first + + if file_exists?(:acme_key) && !global[:force] + bail! do + log "the authorization key for letsencrypt.org already exists" + log "run with --force if you really want to register a new key." end - cert.openssl_body.extensions.each do |ext| - if ext.oid == "subjectAltName" - ips = [] - dns_names = [] - ext.value.split(",").each do |value| - value.strip! - ips << $1 if value =~ /^IP Address:(.*)$/ - dns_names << $1 if value =~ /^DNS:(.*)$/ - end - dns_names.sort! - if ips.first != node.ip_address - log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})" - return true - elsif dns_names != dns_names_for_node(node) - log :updating, "cert for node '#{node.name}' because domain name aliases have changed\n from: #{dns_names.inspect}\n to: #{dns_names_for_node(node).inspect})" - return true - end + else + private_key = Acme.new_private_key + registration = nil + + log(:registering, "letsencrypt.org authorization key using contact `%s`" % contact) do + acme = Acme.new(key: private_key) + registration = acme.register(contact) + if registration + log 'success!', :color => :green, :style => :bold + else + bail! "could not register authorization key." end end - end - return false - end - def created_by_authority?(cert, ca) - authority_key_id = cert.extensions["authorityKeyIdentifier"].identifier.sub(/^keyid:/, '') - authority_key_id == public_key_id_for_ca(ca) - end - - # calculate the "key id" for a root CA, that matches the value - # Authority Key Identifier in the x509 extensions of a cert. - def public_key_id_for_ca(ca_cert) - @ca_key_ids ||= {} - @ca_key_ids[ca_cert.object_id] ||= begin - pubkey = ca_cert.key_material.public_key - seq = OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::Integer.new(pubkey.n), - OpenSSL::ASN1::Integer.new(pubkey.e) - ]) - Digest::SHA1.hexdigest(seq.to_der).upcase.scan(/../).join(':') + log :saving, "authorization key for letsencrypt.org" do + write_file!(:acme_key, private_key.to_pem) + write_file!(:acme_info, JSON.sorted_generate({ + id: registration.id, + contact: registration.contact, + key: registration.key, + uri: registration.uri + })) + log :warning, "keep key file private!" + end end end - def warn_if_commercial_cert_will_soon_expire(node) - dns_names_for_node(node).each do |domain| - if file_exists?([:commercial_cert, domain]) - cert = load_certificate_file([:commercial_cert, domain]) - path = Path.relative_path([:commercial_cert, domain]) - if cert.not_after < Time.now.utc - log :error, "the commercial certificate '#{path}' has EXPIRED! " + - "You should renew it with `leap cert csr --domain #{domain}`." - elsif cert.not_after < Time.now.advance(:months => 2) - log :warning, "the commercial certificate '#{path}' will expire soon (#{cert.not_after}). "+ - "You should renew it with `leap cert csr --domain #{domain}`." - end - end + def assert_no_errors!(msg) + yield + rescue StandardError => exc + bail! :error, msg do + log exc.to_s end end - def generate_cert_for_node(node) - return if node.x509.use == false - - cert = CertificateAuthority::Certificate.new - - # set subject - cert.subject.common_name = node.domain.full - cert.serial_number.number = cert_serial_number(node.domain.full) + def do_renew_cert(global, options, args) + require 'leap_cli/acme' + require 'leap_cli/ssh' + require 'socket' + require 'net/http' - # set expiration - cert.not_before = yesterday - cert.not_after = yesterday_advance(provider.ca.server_certificates.life_span) + csr = nil + account_key = nil + cert = nil + acme = nil - # generate key - cert.key_material.generate_key(provider.ca.server_certificates.bit_size) - - # sign - cert.parent = ca_root - cert.sign!(server_signing_profile(node)) - - # save - write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem) - write_file!([:node_x509_cert, node.name], cert.to_pem) - end - - # - # yields client key and cert suitable for testing - # - def generate_test_client_cert(prefix=nil) - cert = CertificateAuthority::Certificate.new - cert.serial_number.number = cert_serial_number(provider.domain) - cert.subject.common_name = [prefix, random_common_name(provider.domain)].join - cert.not_before = yesterday - cert.not_after = yesterday.advance(:years => 1) - cert.key_material.generate_key(1024) # just for testing, remember! - cert.parent = client_ca_root - cert.sign! client_test_signing_profile - yield cert.key_material.private_key.to_pem, cert.to_pem - end - - # - # creates a CSR and returns it. - # with the correct extReq attribute so that the CA - # doens't generate certs with extensions we don't want. - # - def create_csr(dn, keypair, digest) - csr = CertificateAuthority::SigningRequest.new - csr.distinguished_name = dn - csr.key_material = keypair - csr.digest = digest - - # define extensions manually (library doesn't support setting these on CSRs) - extensions = [] - extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic| - basic.ca = false - } - extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage| - keyusage.usage = ["digitalSignature", "keyEncipherment"] - } - extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage| - extkeyusage.usage = [ "serverAuth"] - } - - # convert extensions to attribute 'extReq' - # aka "Requested Extensions" - factory = OpenSSL::X509::ExtensionFactory.new - attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence( - extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)} - )]) - attrs = [ - OpenSSL::X509::Attribute.new("extReq", attrval), - ] - csr.attributes = attrs - - return csr - end + # + # sanity check the domain + # + domain = args.first + nodes = nodes_for_domain(domain) + domain_ready_for_acme!(domain) - def ca_root - @ca_root ||= begin - load_certificate_file(:ca_cert, :ca_key) + # + # load key material + # + assert_files_exist!([:commercial_key, domain], [:commercial_csr, domain], + :msg => 'Please create the CSR first with `leap cert csr %s`' % domain) + assert_no_errors!("Could not load #{path([:commercial_csr, domain])}") do + csr = Acme.load_csr(read_file!([:commercial_csr, domain])) end - end - - def client_ca_root - @client_ca_root ||= begin - load_certificate_file(:client_ca_cert, :client_ca_key) + assert_files_exist!(:acme_key, + :msg => "Please run `leap cert register` first. This only needs to be done once.") + assert_no_errors!("Could not load #{path(:acme_key)}") do + account_key = Acme.load_private_key(read_file!(:acme_key)) end - end - def load_certificate_file(crt_file, key_file=nil, password=nil) - crt = read_file!(crt_file) - openssl_cert = OpenSSL::X509::Certificate.new(crt) - cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) - if key_file - key = read_file!(key_file) - cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password) + # + # check authorization for this domain + # + log :checking, "authorization" + acme = Acme.new(domain: domain, key: account_key) + status, message = acme.authorize do |challenge| + log(:uploading, 'challenge to server %s' % domain) do + SSH.remote_command(nodes) do |ssh, host| + ssh.scripts.upload_acme_challenge(challenge.token, challenge.file_content) + end + end + log :waiting, "for letsencrypt.org to verify challenge" + end + if status == 'valid' + log 'authorized!', color: :green, style: :bold + elsif status == 'error' + bail! :error, message + elsif status == 'unauthorized' + bail!(:unauthorized, message, color: :yellow, style: :bold) do + log 'You must first run `leap cert register` to register the account key with letsencrypt.org' + end end - return cert - end - - def ca_root_signing_profile - { - "extensions" => { - "basicConstraints" => {"ca" => true}, - "keyUsage" => { - "usage" => ["critical", "keyCertSign"] - }, - "extendedKeyUsage" => { - "usage" => [] - } - } - } - end - # - # For keyusage, openvpn server certs can have keyEncipherment or keyAgreement. - # Web browsers seem to break without keyEncipherment. - # For now, I am using digitalSignature + keyEncipherment - # - # * digitalSignature -- for (EC)DHE cipher suites - # "The digitalSignature bit is asserted when the subject public key is used - # with a digital signature mechanism to support security services other - # than certificate signing (bit 5), or CRL signing (bit 6). Digital - # signature mechanisms are often used for entity authentication and data - # origin authentication with integrity." - # - # * keyEncipherment ==> for plain RSA cipher suites - # "The keyEncipherment bit is asserted when the subject public key is used for - # key transport. For example, when an RSA key is to be used for key management, - # then this bit is set." - # - # * keyAgreement ==> for used with DH, not RSA. - # "The keyAgreement bit is asserted when the subject public key is used for key - # agreement. For example, when a Diffie-Hellman key is to be used for key - # management, then this bit is set." - # - # digest options: SHA512, SHA256, SHA1 - # - def server_signing_profile(node) - { - "digest" => provider.ca.server_certificates.digest, - "extensions" => { - "keyUsage" => { - "usage" => ["digitalSignature", "keyEncipherment"] - }, - "extendedKeyUsage" => { - "usage" => ["serverAuth", "clientAuth"] - }, - "subjectAltName" => { - "ips" => [node.ip_address], - "dns_names" => dns_names_for_node(node) - } - } - } + log :fetching, "new certificate from letsencrypt.org" + assert_no_errors!("could not renew certificate") do + cert = acme.get_certificate(csr) + end + log 'success', color: :green, style: :bold + write_file!([:commercial_cert, domain], cert.fullchain_to_pem) + log 'You should now run `leap deploy` to deploy the new certificate.' end # - # This is used when signing the main cert for the provider's domain - # with our own CA (for testing purposes). Typically, this cert would - # be purchased from a commercial CA, and not signed this way. + # Returns a hash of nodes that match this domain. It also checks: # - def domain_test_signing_profile - { - "digest" => "SHA256", - "extensions" => { - "keyUsage" => { - "usage" => ["digitalSignature", "keyEncipherment"] - }, - "extendedKeyUsage" => { - "usage" => ["serverAuth"] - } - } - } - end - + # * a node configuration has this domain + # * the dns for the domain exists # - # This is used when signing a dummy client certificate that is only to be - # used for testing. + # This method will bail if any checks fail. # - def client_test_signing_profile - { - "digest" => "SHA256", - "extensions" => { - "keyUsage" => { - "usage" => ["digitalSignature"] - }, - "extendedKeyUsage" => { - "usage" => ["clientAuth"] - } - } - } - end - - def dns_names_for_node(node) - names = [node.domain.internal, node.domain.full] - if node['dns'] && node.dns['aliases'] && node.dns.aliases.any? - names += node.dns.aliases + def nodes_for_domain(domain) + bail! { log 'Argument DOMAIN is required' } if domain.nil? || domain.empty? + nodes = manager.nodes['dns.aliases' => domain] + if nodes.empty? + bail! :error, "There are no nodes configured for domain `%s`" % domain end - names.compact! - names.sort! - names.uniq! - return names + begin + ips = Socket.getaddrinfo(domain, 'http').map {|record| record[2]}.uniq + nodes = nodes['ip_address' => ips] + if nodes.empty? + bail! do + log :error, "The domain `%s` resolves to [%s]" % [domain, ips.join(', ')] + log :error, "But there no nodes configured for this domain with these adddresses." + end + end + rescue SocketError + bail! :error, "Could not resolve the DNS for `#{domain}`. Without a DNS " + + "entry for this domain, authorization will not work." + end + return nodes end # - # For cert serial numbers, we need a non-colliding number less than 160 bits. - # md5 will do nicely, since there is no need for a secure hash, just a short one. - # (md5 is 128 bits) + # runs the following checks on the domain: # - def cert_serial_number(domain_name) - Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16) - end - + # * we are able to get /.well-known/acme-challenge/ok # - # for the random common name, we need a text string that will be unique across all certs. - # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid + # This method will bail if any checks fail. # - def random_common_name(domain_name) - cert_serial_number(domain_name).to_s(36) - end - - # prints CertificateAuthority::DistinguishedName fields - def print_dn(dn) - fields = {} - [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr| - fields[attr] = dn.send(attr) if dn.send(attr) - end - fields.inspect - end - - ## - ## TIME HELPERS - ## - ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet - ## are behind UTC. - ## - - def yesterday - t = Time.now - 24*24*60 - Time.utc t.year, t.month, t.day - end - - def yesterday_advance(string) - number, unit = string.split(' ') - unless ['years', 'months', 'days', 'hours', 'minutes'].include? unit - bail!("The time property '#{string}' is missing a unit (one of: years, months, days, hours, minutes).") - end - unless number.to_i.to_s == number - bail!("The time property '#{string}' is missing a number.") + def domain_ready_for_acme!(domain) + begin + uri = URI("https://#{domain}/.well-known/acme-challenge/ok") + options = { + use_ssl: true, + open_timeout: 5, + verify_mode: OpenSSL::SSL::VERIFY_NONE + } + Net::HTTP.start(uri.host, uri.port, options) do |http| + http.request(Net::HTTP::Get.new(uri)) do |response| + if !response.is_a?(Net::HTTPSuccess) + bail!(:error, "Could not GET %s" % uri) do + log "%s %s" % [response.code, response.message] + log "You may need to run `leap deploy`" + end + end + end + end + rescue Errno::ETIMEDOUT, Net::OpenTimeout + bail! :error, "Connection attempt timed out: %s" % uri + rescue Interrupt + bail! + rescue StandardError => exc + bail!(:error, "Could not GET %s" % uri) do + log exc.to_s + end end - yesterday.advance(unit.to_sym => number.to_i) end end; end |