class GitHubPages::HealthCheck::Domain
Constants
- CURRENT_IPV6_ADDRESSES
- CURRENT_IP_ADDRESSES
- CURRENT_IP_ADDRESSES_ALL
- HASH_METHODS
- LEGACY_IP_ADDRESSES
- REQUESTED_RECORD_TYPES
Attributes
Public Class Methods
# File lib/github-pages-health-check/domain.rb, line 105 def initialize(host, nameservers: :default) unless host.is_a? String raise ArgumentError, "Expected string, got #{host.class}" end @host = normalize_host(host) @nameservers = nameservers @resolver = GitHubPages::HealthCheck::Resolver.new(self.host, :nameservers => nameservers) end
# File lib/github-pages-health-check/domain.rb, line 101 def self.redundant(host) GitHubPages::HealthCheck::RedundantCheck.new(host).check end
Public Instance Methods
Is this domain’s first response an A record?
# File lib/github-pages-health-check/domain.rb, line 371 def a_record? return @is_a_record if defined?(@is_a_record) return unless dns? @is_a_record = Dnsruby::Types::A == dns.first.type end
Does this domain has an A record setup (not necessarily as the first record)?
# File lib/github-pages-health-check/domain.rb, line 387 def a_record_present? return unless dns? dns.any? { |answer| answer.type == Dnsruby::Types::A && answer.name.to_s == host } end
Is this domain’s first response an AAAA record?
# File lib/github-pages-health-check/domain.rb, line 379 def aaaa_record? return @is_aaaa_record if defined?(@is_aaaa_record) return unless dns? @is_aaaa_record = Dnsruby::Types::AAAA == dns.first.type end
Does this domain has an AAAA record setup (not necessarily as the first record)?
# File lib/github-pages-health-check/domain.rb, line 394 def aaaa_record_present? return unless dns? dns.any? { |answer| answer.type == Dnsruby::Types::AAAA && answer.name.to_s == host } end
Is this domain an apex domain, meaning a CNAME would be inappropriate
# File lib/github-pages-health-check/domain.rb, line 174 def apex_domain? return @apex_domain if defined?(@apex_domain) return false unless valid_domain? return true if dns_zone_soa? && dns_zone_ns? # PublicSuffix.domain pulls out the apex-level domain name. # E.g. PublicSuffix.domain("techblog.netflix.com") # => "netflix.com" # It's aware of multi-step top-level domain names: # E.g. PublicSuffix.domain("blog.digital.gov.uk") # => "digital.gov.uk" # For apex-level domain names, DNS providers do not support CNAME records. unicode_host = Addressable::IDNA.to_unicode(host) PublicSuffix.domain(unicode_host, :default_rule => nil, :ignore_private => true) == unicode_host end
Any errors querying CAA
records
# File lib/github-pages-health-check/domain.rb, line 486 def caa_error return nil unless caa&.errored? caa.error.class.name end
Runs all checks, raises an error if invalid rubocop:disable Metrics/AbcSize
# File lib/github-pages-health-check/domain.rb, line 118 def check! raise Errors::InvalidDomainError.new :domain => self unless valid_domain? raise Errors::InvalidDNSError.new :domain => self unless dns_resolves? raise Errors::DeprecatedIPError.new :domain => self if deprecated_ip? return true if proxied? raise Errors::InvalidARecordError.new :domain => self if invalid_a_record? raise Errors::InvalidCNAMEError.new :domain => self if invalid_cname? raise Errors::InvalidAAAARecordError.new :domain => self if invalid_aaaa_record? raise Errors::NotServedByPagesError.new :domain => self unless served_by_pages? true end
Does the domain resolve to a CloudFlare-owned IP
# File lib/github-pages-health-check/domain.rb, line 299 def cloudflare_ip? cdn_ip?(CloudFlare) end
The domain to which this domain’s CNAME resolves Returns nil if the domain is not a CNAME
# File lib/github-pages-health-check/domain.rb, line 411 def cname return unless dns? cnames = dns.take_while { |answer| answer.type == Dnsruby::Types::CNAME } return if cnames.empty? www_cname(cnames.last) @cname ||= Domain.new(cnames.last.cname.to_s) end
Is this domain’s first response a CNAME record?
# File lib/github-pages-health-check/domain.rb, line 401 def cname_record? return unless dns? return false unless cname cname.valid_domain? end
Check if the CNAME points to a Domain
that points to pages e.g. CNAME -> Domain
-> Pages rubocop:disable Metrics/AbcSize
# File lib/github-pages-health-check/domain.rb, line 249 def cname_to_domain_to_pages? return false unless dns? a_record_to_pages = dns.select { |d| d.type == Dnsruby::Types::A && d.name.to_s == host }.first return false unless a_record_to_pages && cname? && !cname_to_pages_dot_github_dot_com? && @www_cname CURRENT_IP_ADDRESSES.include?(a_record_to_pages.address.to_s.downcase) end
Is the given domain CNAME’d directly to our Fastly
account?
# File lib/github-pages-health-check/domain.rb, line 269 def cname_to_fastly? cname? && !pages_domain? && cname.fastly? end
Is the domain’s first response a CNAME to a pages domain?
# File lib/github-pages-health-check/domain.rb, line 242 def cname_to_github_user_domain? cname? && !cname_to_pages_dot_github_dot_com? && cname.pages_domain? end
Is the given domain a CNAME to pages.github.(io|com) instead of being CNAME’d to the user’s subdomain?
domain - the domain to check, generally the target of a cname
# File lib/github-pages-health-check/domain.rb, line 264 def cname_to_pages_dot_github_dot_com? cname? && cname.pages_dot_github_dot_com? end
rubocop:enable Metrics/AbcSize
# File lib/github-pages-health-check/domain.rb, line 132 def deprecated_ip? return @deprecated_ip if defined? @deprecated_ip @deprecated_ip = (valid_domain? && a_record? && old_ip_address?) end
Returns an array of DNS answers
# File lib/github-pages-health-check/domain.rb, line 338 def dns return @dns if defined? @dns return unless valid_domain? @dns = Timeout.timeout(TIMEOUT) do GitHubPages::HealthCheck.without_warnings do next if host.nil? REQUESTED_RECORD_TYPES .map { |type| resolver.query(type) } .flatten.uniq end end rescue StandardError @dns = nil end
Are we even able to get the DNS record?
# File lib/github-pages-health-check/domain.rb, line 356 def dns? !(dns.nil? || dns.empty?) end
Does the domain have associated NS records?
# File lib/github-pages-health-check/domain.rb, line 207 def dns_zone_ns? return @ns_records if defined?(@ns_records) return false unless dns? @ns_records = dns.any? do |answer| answer.type == Dnsruby::Types::NS && answer.name.to_s == host end end
Does the domain have an associated SOA record?
# File lib/github-pages-health-check/domain.rb, line 195 def dns_zone_soa? return @soa_records if defined?(@soa_records) return false unless dns? @soa_records = dns.any? do |answer| answer.type == Dnsruby::Types::SOA && answer.name.to_s == host end end
Does this domain redirect HTTP requests to HTTPS?
# File lib/github-pages-health-check/domain.rb, line 463 def enforces_https? return false unless https? && http_response.headers["Location"] redirect = Addressable::URI.parse(http_response.headers["Location"]) redirect.scheme == "https" && redirect.host == host end
Is the host our Fastly
CNAME?
# File lib/github-pages-health-check/domain.rb, line 294 def fastly? !!host.match(/\A#{Regexp.union(Fastly::HOSTNAMES)}\z/i) end
Does the domain resolve to a Fastly-owned IP
# File lib/github-pages-health-check/domain.rb, line 304 def fastly_ip? cdn_ip?(Fastly) end
Is this domain owned by GitHub?
# File lib/github-pages-health-check/domain.rb, line 289 def github_domain? host.downcase.eql?("github.com") || host.downcase.end_with?(".github.com") end
Does this domain respond to HTTPS requests with a valid cert?
# File lib/github-pages-health-check/domain.rb, line 452 def https? https_response.return_code == :ok end
Can an HTTPS certificate be issued for this domain?
# File lib/github-pages-health-check/domain.rb, line 471 def https_eligible? # Can't have any IP's which aren't GitHub's present. return false if non_github_pages_ip_present? # Can't have underscores in the domain name (Let's Encrypt does not allow it) return false if host.include?("_") # Must be a CNAME or point to our IPs. return true if cname_to_github_user_domain? || cname_to_domain_to_pages? # Check CAA records for the full domain and its parent domain. pointed_to_github_pages_ip? && caa.lets_encrypt_allowed? end
The response code of the HTTPS request, if it failed. Useful for diagnosing cert errors
# File lib/github-pages-health-check/domain.rb, line 458 def https_error https_response.return_code unless https? end
# File lib/github-pages-health-check/domain.rb, line 144 def invalid_a_record? return @invalid_a_record if defined? @invalid_a_record @invalid_a_record = (valid_domain? && a_record_present? && !should_be_a_record?) end
# File lib/github-pages-health-check/domain.rb, line 138 def invalid_aaaa_record? return @invalid_aaaa_record if defined? @invalid_aaaa_record @invalid_aaaa_record = (valid_domain? && aaaa_record_present? && !should_be_a_record?) end
# File lib/github-pages-health-check/domain.rb, line 150 def invalid_cname? return @invalid_cname if defined? @invalid_cname @invalid_cname = begin return false unless valid_domain? return false if github_domain? || apex_domain? return true if cname_to_pages_dot_github_dot_com? || cname_to_fastly? !cname_to_github_user_domain? && should_be_cname_record? end end
# File lib/github-pages-health-check/domain.rb, line 427 def mx_records_present? return unless dns? dns.any? { |answer| answer.type == Dnsruby::Types::MX } end
Are any of the domain’s A or AAAA records pointing elsewhere?
# File lib/github-pages-health-check/domain.rb, line 233 def non_github_pages_ip_present? return unless dns? dns .select { |a| Dnsruby::Types::A == a.type || Dnsruby::Types::AAAA == a.type } .any? { |a| !github_pages_ip?(a.address.to_s) } end
Does this domain have any A record that points to the legacy IPs?
# File lib/github-pages-health-check/domain.rb, line 362 def old_ip_address? return unless dns? dns.any? do |answer| answer.type == Dnsruby::Types::A && legacy_ip?(answer.address.to_s) end end
Is the host a *.github.(io|com) domain?
# File lib/github-pages-health-check/domain.rb, line 279 def pages_domain? !!host.match(/\A[\w-]+\.github\.(io|com)\.?\z/i) end
Is the host pages.github.com or pages.github.io?
# File lib/github-pages-health-check/domain.rb, line 284 def pages_dot_github_dot_com? !!host.match(/\Apages\.github\.(io|com)\.?\z/i) end
Is the host a *.github.io domain?
# File lib/github-pages-health-check/domain.rb, line 274 def pages_io_domain? !!host.match(/\A[\w-]+\.github\.(io)\.?\z/i) end
Is the domain’s first response an A or AAAA record to a valid GitHub Pages IP?
# File lib/github-pages-health-check/domain.rb, line 226 def pointed_to_github_pages_ip? return false unless address_record? CURRENT_IP_ADDRESSES_ALL.include?(dns.first.address.to_s.downcase) end
Does this non-GitHub-pages domain proxy a GitHub Pages site?
This can be:
1. A Cloudflare-owned IP address 2. A site that returns GitHub.com server headers, but isn't CNAME'd to a GitHub domain 3. A site that returns GitHub.com server headers, but isn't CNAME'd to a GitHub IP
# File lib/github-pages-health-check/domain.rb, line 316 def proxied? return unless dns? return true if cloudflare_ip? return false if pointed_to_github_pages_ip? return false if cname_to_github_user_domain? return false if cname_to_domain_to_pages? return false if cname_to_pages_dot_github_dot_com? return false if cname_to_fastly? || fastly_ip? served_by_pages? end
# File lib/github-pages-health-check/domain.rb, line 433 def served_by_pages? return @served_by_pages if defined? @served_by_pages return unless dns_resolves? @served_by_pages = begin return true if response.headers["Server"] == "GitHub.com" # Typhoeus mangles the case of the header, compare insensitively response.headers.any? { |k, _v| k.downcase == "x-github-request-id" } end end
Should the domain use an A record?
# File lib/github-pages-health-check/domain.rb, line 217 def should_be_a_record? !pages_io_domain? && (apex_domain? || mx_records_present?) end
# File lib/github-pages-health-check/domain.rb, line 221 def should_be_cname_record? !should_be_a_record? end
# File lib/github-pages-health-check/domain.rb, line 445 def uri(overrides = {}) options = { :host => host, :scheme => scheme, :path => "/" } options = options.merge(overrides) Addressable::URI.new(options).normalize.to_s end
Is this a valid domain that PublicSuffix recognizes? Used as an escape hatch to prevent false positives on DNS checks
# File lib/github-pages-health-check/domain.rb, line 164 def valid_domain? return @valid if defined? @valid unicode_host = Addressable::IDNA.to_unicode(host) @valid = PublicSuffix.valid?(unicode_host, :default_rule => nil, :ignore_private => true) end
Check if we have a ‘www.’ CNAME that matches the domain
# File lib/github-pages-health-check/domain.rb, line 422 def www_cname(cname) @www_cname ||= cname.name.to_s.start_with?("www.") && cname.name.to_s.end_with?(cname.domainname.to_s) end
Private Instance Methods
Adjust ‘domain` so that it won’t be searched for with /etc/resolv.conf
GitHubPages::HealthCheck.new("anything.io").absolute_domain => "anything.io."
# File lib/github-pages-health-check/domain.rb, line 560 def absolute_domain host.end_with?(".") ? host : "#{host}." end
# File lib/github-pages-health-check/domain.rb, line 494 def address_record? a_record? || aaaa_record? end
# File lib/github-pages-health-check/domain.rb, line 498 def caa @caa ||= GitHubPages::HealthCheck::CAA.new( :host => cname&.host || host, :nameservers => nameservers ) end
Does the domain resolve to a CDN-owned IP
# File lib/github-pages-health-check/domain.rb, line 569 def cdn_ip?(cdn) return unless dns? address_records = dns.select do |answer| Dnsruby::Types::A == answer.type || Dnsruby::Types::AAAA == answer.type end return false if !address_records || address_records.empty? address_records.all? do |answer| cdn.controls_ip?(answer.address) end end
# File lib/github-pages-health-check/domain.rb, line 586 def github_pages_ip?(ip_addr) CURRENT_IP_ADDRESSES_ALL.include?(ip_addr&.to_s&.downcase) end
The domain’s response to HTTP requests, without following redirects
# File lib/github-pages-health-check/domain.rb, line 521 def http_response options = GitHubPages::HealthCheck.typhoeus_options.merge(:followlocation => false) @http_response ||= Typhoeus.head(uri(:scheme => "http"), options) end
The domain’s response to HTTPS requests, without following redirects
# File lib/github-pages-health-check/domain.rb, line 527 def https_response options = GitHubPages::HealthCheck.typhoeus_options.merge(:followlocation => false) @https_response ||= Typhoeus.head(uri(:scheme => "https"), options) end
# File lib/github-pages-health-check/domain.rb, line 582 def legacy_ip?(ip_addr) LEGACY_IP_ADDRESSES.include?(ip_addr) end
Parse the URI. Accept either domain names or full URI’s. Used by the initializer so we can be more flexible with inputs.
domain - a URI or domain name.
Examples
normalize_host("benbalter.github.com") # => 'benbalter.github.com' normalize_host("https://benbalter.github.com") # => 'benbalter.github.com' normalize_host("benbalter.github.com/help-me-im-a-path/") # => 'benbalter.github.com'
Return the hostname.
# File lib/github-pages-health-check/domain.rb, line 547 def normalize_host(domain) domain = domain.strip.chomp(".") host = Addressable::URI.parse(domain).normalized_host host ||= Addressable::URI.parse("http://#{domain}").normalized_host host unless host.to_s.empty? rescue Addressable::URI::InvalidURIError nil end
The domain’s response to HTTP(S) requests, following redirects
# File lib/github-pages-health-check/domain.rb, line 506 def response return @response if defined? @response @response = Typhoeus.head(uri, GitHubPages::HealthCheck.typhoeus_options) # Workaround for webmock not playing nicely with Typhoeus redirects # See https://github.com/bblimke/webmock/issues/237 if @response.mock? && @response.headers["Location"] @response = Typhoeus.head(response.headers["Location"], GitHubPages::HealthCheck.typhoeus_options) end @response end
# File lib/github-pages-health-check/domain.rb, line 564 def scheme @scheme ||= github_domain? ? "https" : "http" end