github-pages-health-check 0.6.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +3 -0
  7. data/README.md +83 -0
  8. data/github-pages-health-check.gemspec +28 -0
  9. data/lib/github-pages-health-check.rb +18 -245
  10. data/lib/github-pages-health-check/checkable.rb +62 -0
  11. data/lib/github-pages-health-check/cloudflare.rb +11 -7
  12. data/lib/github-pages-health-check/domain.rb +260 -0
  13. data/lib/github-pages-health-check/error.rb +7 -6
  14. data/lib/github-pages-health-check/errors.rb +13 -0
  15. data/lib/github-pages-health-check/errors/build_error.rb +8 -0
  16. data/lib/github-pages-health-check/errors/deprecated_ip_error.rb +11 -0
  17. data/lib/github-pages-health-check/errors/invalid_a_record_error.rb +11 -0
  18. data/lib/github-pages-health-check/errors/invalid_cname_error.rb +11 -0
  19. data/lib/github-pages-health-check/errors/invalid_dns_error.rb +11 -0
  20. data/lib/github-pages-health-check/errors/invalid_domain_error.rb +11 -0
  21. data/lib/github-pages-health-check/errors/invalid_repository_error.rb +11 -0
  22. data/lib/github-pages-health-check/errors/missing_access_token_error.rb +11 -0
  23. data/lib/github-pages-health-check/errors/not_served_by_pages_error.rb +11 -0
  24. data/lib/github-pages-health-check/printer.rb +71 -0
  25. data/lib/github-pages-health-check/repository.rb +74 -0
  26. data/lib/github-pages-health-check/site.rb +32 -0
  27. data/lib/github-pages-health-check/version.rb +3 -3
  28. data/script/bootstrap +5 -0
  29. data/script/check +11 -0
  30. data/script/check-cloudflare-ips +17 -0
  31. data/script/cibuild +9 -0
  32. data/script/console +5 -0
  33. data/script/release +42 -0
  34. data/script/test +2 -0
  35. data/script/update-cloudflare-ips +14 -0
  36. metadata +60 -7
  37. data/lib/github-pages-health-check/errors/deprecated_ip.rb +0 -9
  38. data/lib/github-pages-health-check/errors/invalid_a_record.rb +0 -9
  39. data/lib/github-pages-health-check/errors/invalid_cname.rb +0 -9
  40. data/lib/github-pages-health-check/errors/invalid_dns.rb +0 -9
  41. data/lib/github-pages-health-check/errors/not_served_by_pages.rb +0 -9
@@ -1,9 +1,10 @@
1
- class GitHubPages
2
- class HealthCheck
1
+ module GitHubPages
2
+ module HealthCheck
3
3
  class CloudFlare
4
4
  include Singleton
5
5
 
6
- CONFIG_PATH = File.expand_path("../../config/cloudflare-ips.txt", File.dirname(__FILE__))
6
+ # Internal: The path of the config file.
7
+ attr_reader :path
7
8
 
8
9
  # Public: Does cloudflare control this address?
9
10
  def self.controls_ip?(address)
@@ -12,17 +13,16 @@ class GitHubPages
12
13
 
13
14
  # Internal: Create a new cloudflare info instance.
14
15
  def initialize(options = {})
15
- @path = options.fetch(:path) { CONFIG_PATH }
16
+ @path = options.fetch(:path) { default_config_path }
16
17
  end
17
18
 
18
- # Internal: The path of the config file.
19
- attr_reader :path
20
-
21
19
  # Internal: Does cloudflare control this address?
22
20
  def controls_ip?(address)
23
21
  ranges.any? { |range| range.include?(address) }
24
22
  end
25
23
 
24
+ private
25
+
26
26
  # Internal: The IP address ranges that cloudflare controls.
27
27
  def ranges
28
28
  @ranges ||= load_ranges
@@ -32,6 +32,10 @@ class GitHubPages
32
32
  def load_ranges
33
33
  File.read(path).lines.map { |line| IPAddr.new(line.chomp) }
34
34
  end
35
+
36
+ def default_config_path
37
+ File.expand_path("../../config/cloudflare-ips.txt", File.dirname(__FILE__))
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,260 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ class Domain < Checkable
4
+
5
+ attr_reader :host
6
+
7
+ LEGACY_IP_ADDRESSES = %w[
8
+ 207.97.227.245
9
+ 204.232.175.78
10
+ 199.27.73.133
11
+ ].freeze
12
+
13
+ CURRENT_IP_ADDRESSES = %w[
14
+ 192.30.252.153
15
+ 192.30.252.154
16
+ ].freeze
17
+
18
+ HASH_METHODS = [
19
+ :host, :uri, :dns_resolves?, :proxied?, :cloudflare_ip?,
20
+ :old_ip_address?, :a_record?, :cname_record?, :valid_domain?,
21
+ :apex_domain?, :should_be_a_record?, :cname_to_github_user_domain?,
22
+ :cname_to_pages_dot_github_dot_com?, :cname_to_fastly?,
23
+ :pointed_to_github_pages_ip?, :pages_domain?, :served_by_pages?,
24
+ :valid_domain?
25
+ ].freeze
26
+
27
+ def initialize(host)
28
+ unless host.is_a? String
29
+ raise ArgumentError, "Expected string, got #{host.class}"
30
+ end
31
+
32
+ @host = host_from_uri(host)
33
+ end
34
+
35
+ # Runs all checks, raises an error if invalid
36
+ def check!
37
+ raise Errors::InvalidDNSError unless dns_resolves?
38
+ return true if proxied?
39
+ raise Errors::DeprecatedIPError if deprecated_ip?
40
+ raise Errors::InvalidARecordError if invalid_a_record?
41
+ raise Errors::InvalidCNAMEError if invalid_cname?
42
+ raise Errors::NotServedByPagesError unless served_by_pages?
43
+ true
44
+ end
45
+
46
+ def deprecated_ip?
47
+ return @deprecated_ip if defined? @deprecated_ip
48
+ @deprecated_ip = (a_record? && old_ip_address?)
49
+ end
50
+
51
+ def invalid_a_record?
52
+ return @invalid_a_record if defined? @invalid_a_record
53
+ @invalid_a_record = (valid_domain? && a_record? && !should_be_a_record?)
54
+ end
55
+
56
+ def invalid_cname?
57
+ return @invalid_cname if defined? @invalid_cname
58
+ @invalid_cname = begin
59
+ return false unless valid_domain?
60
+ return false if github_domain? || apex_domain?
61
+ return true if cname_to_pages_dot_github_dot_com? || cname_to_fastly?
62
+ !cname_to_github_user_domain?
63
+ end
64
+ end
65
+
66
+ # Is this a valid domain that PublicSuffix recognizes?
67
+ # Used as an escape hatch to prevent false positives on DNS checkes
68
+ def valid_domain?
69
+ return @valid if defined? @valid
70
+ @valid = PublicSuffix.valid?(host)
71
+ end
72
+
73
+ # Is this domain an apex domain, meaning a CNAME would be innapropriate
74
+ def apex_domain?
75
+ return @apex_domain if defined?(@apex_domain)
76
+
77
+ answers = Resolv::DNS.open { |dns|
78
+ dns.getresources(absolute_domain, Resolv::DNS::Resource::IN::NS)
79
+ }
80
+
81
+ @apex_domain = answers.any?
82
+ end
83
+
84
+ # Should the domain be an apex record?
85
+ def should_be_a_record?
86
+ !pages_domain? && apex_domain?
87
+ end
88
+
89
+ # Is the domain's first response an A record to a valid GitHub Pages IP?
90
+ def pointed_to_github_pages_ip?
91
+ a_record? && CURRENT_IP_ADDRESSES.include?(dns.first.value)
92
+ end
93
+
94
+ # Is the domain's first response a CNAME to a pages domain?
95
+ def cname_to_github_user_domain?
96
+ cname? && !cname_to_pages_dot_github_dot_com? && cname.pages_domain?
97
+ end
98
+
99
+ # Is the given domain a CNAME to pages.github.(io|com)
100
+ # instead of being CNAME'd to the user's subdomain?
101
+ #
102
+ # domain - the domain to check, generaly the target of a cname
103
+ def cname_to_pages_dot_github_dot_com?
104
+ cname? && cname.pages_dot_github_dot_com?
105
+ end
106
+
107
+ # Is the given domain CNAME'd directly to our Fastly account?
108
+ def cname_to_fastly?
109
+ cname? && !pages_domain? && cname.fastly?
110
+ end
111
+
112
+ # Is the host a *.github.io domain?
113
+ def pages_domain?
114
+ !!host.match(/\A[\w-]+\.github\.(io|com)\.?\z/i)
115
+ end
116
+
117
+ # Is the host pages.github.com or pages.github.io?
118
+ def pages_dot_github_dot_com?
119
+ !!host.match(/\Apages\.github\.(io|com)\.?\z/i)
120
+ end
121
+
122
+ # Is this domain owned by GitHub?
123
+ def github_domain?
124
+ !!host.match(/\.github\.com\z/)
125
+ end
126
+
127
+ # Is the host our Fastly CNAME?
128
+ def fastly?
129
+ !!host.match(/\Agithub\.map\.fastly\.net\.?\z/i)
130
+ end
131
+
132
+ # Does the domain resolve to a CloudFlare-owned IP
133
+ def cloudflare_ip?
134
+ return unless dns?
135
+ dns.all? do |answer|
136
+ answer.class == Net::DNS::RR::A && CloudFlare.controls_ip?(answer.address)
137
+ end
138
+ end
139
+
140
+ # Does this non-GitHub-pages domain proxy a GitHub Pages site?
141
+ #
142
+ # This can be:
143
+ # 1. A Cloudflare-owned IP address
144
+ # 2. A site that returns GitHub.com server headers, but isn't CNAME'd to a GitHub domain
145
+ # 3. A site that returns GitHub.com server headers, but isn't CNAME'd to a GitHub IP
146
+ def proxied?
147
+ return unless dns?
148
+ return true if cloudflare_ip?
149
+ return false if pointed_to_github_pages_ip? || cname_to_github_user_domain?
150
+ return false if cname_to_pages_dot_github_dot_com? || cname_to_fastly?
151
+ served_by_pages?
152
+ end
153
+
154
+ # Returns an array of DNS answers
155
+ def dns
156
+ return @dns if defined? @dns
157
+ @dns = Timeout.timeout(TIMEOUT) do
158
+ GitHubPages::HealthCheck.without_warnings do
159
+ Net::DNS::Resolver.start(absolute_domain).answer unless host.nil?
160
+ end
161
+ end
162
+ rescue StandardError
163
+ @dns = nil
164
+ end
165
+
166
+ # Are we even able to get the DNS record?
167
+ def dns?
168
+ !(dns.nil? || dns.empty?)
169
+ end
170
+ alias_method :dns_resolves?, :dns?
171
+
172
+ # Does this domain have *any* A record that points to the legacy IPs?
173
+ def old_ip_address?
174
+ dns.any? do |answer|
175
+ answer.class == Net::DNS::RR::A && LEGACY_IP_ADDRESSES.include?(answer.address.to_s)
176
+ end if dns?
177
+ end
178
+
179
+ # Is this domain's first response an A record?
180
+ def a_record?
181
+ return unless dns?
182
+ dns.first.class == Net::DNS::RR::A
183
+ end
184
+
185
+ # Is this domain's first response a CNAME record?
186
+ def cname_record?
187
+ return unless dns?
188
+ dns.first.class == Net::DNS::RR::CNAME
189
+ end
190
+ alias cname? cname_record?
191
+
192
+ # The domain to which this domain's CNAME resolves
193
+ # Returns nil if the domain is not a CNAME
194
+ def cname
195
+ return unless cname?
196
+ @cname ||= Domain.new(dns.first.cname.to_s)
197
+ end
198
+
199
+ def served_by_pages?
200
+ return @served_by_pages if defined? @served_by_pages
201
+
202
+ @served_by_pages = begin
203
+ response = Typhoeus.head(uri, TYPHOEUS_OPTIONS)
204
+
205
+ # Workaround for webmock not playing nicely with Typhoeus redirects
206
+ # See https://github.com/bblimke/webmock/issues/237
207
+ if response.mock? && response.headers["Location"]
208
+ response = Typhoeus.head(response.headers["Location"], TYPHOEUS_OPTIONS)
209
+ end
210
+
211
+ return false unless response.mock? || response.return_code == :ok
212
+ return true if response.headers["Server"] == "GitHub.com"
213
+
214
+ # Typhoeus mangles the case of the header, compare insensitively
215
+ response.headers.any? { |k,v| k =~ /X-GitHub-Request-Id/i }
216
+ end
217
+ end
218
+
219
+ def uri
220
+ @uri ||= begin
221
+ options = { :host => host, :scheme => scheme, :path => "/" }
222
+ Addressable::URI.new(options).normalize.to_s
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ # Parse the URI. Accept either domain names or full URI's.
229
+ # Used by the initializer so we can be more flexible with inputs.
230
+ #
231
+ # domain - a URI or domain name.
232
+ #
233
+ # Examples
234
+ #
235
+ # host_from_uri("benbalter.github.com")
236
+ # # => 'benbalter.github.com'
237
+ # host_from_uri("https://benbalter.github.com")
238
+ # # => 'benbalter.github.com'
239
+ # host_from_uri("benbalter.github.com/help-me-im-a-path/")
240
+ # # => 'benbalter.github.com'
241
+ #
242
+ # Return the hostname.
243
+ def host_from_uri(domain)
244
+ Addressable::URI.parse(domain).host || Addressable::URI.parse("http://#{domain}").host
245
+ end
246
+
247
+ # Adjust `domain` so that it won't be searched for with /etc/resolv.conf's search rules.
248
+ #
249
+ # GitHubPages::HealthCheck.new("anything.io").absolute_domain
250
+ # => "anything.io."
251
+ def absolute_domain
252
+ host.end_with?(".") ? host : "#{host}."
253
+ end
254
+
255
+ def scheme
256
+ @scheme ||= github_domain? ? "https" : "http"
257
+ end
258
+ end
259
+ end
260
+ end
@@ -1,11 +1,12 @@
1
- class GitHubPages
2
- class HealthCheck
1
+ module GitHubPages
2
+ module HealthCheck
3
3
  class Error < StandardError
4
- def message
5
- "Invalid domain"
4
+ def self.inherited(base)
5
+ subclasses << base
6
6
  end
7
- def to_s
8
- message
7
+
8
+ def self.subclasses
9
+ @subclasses ||= []
9
10
  end
10
11
  end
11
12
  end
@@ -0,0 +1,13 @@
1
+ Dir[File.expand_path("../errors/*_error.rb", __FILE__)].each do |f|
2
+ require f
3
+ end
4
+
5
+ module GitHubPages
6
+ module HealthCheck
7
+ module Errors
8
+ def self.all
9
+ Error.subclasses
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class BuildError < GitHubPages::HealthCheck::Error
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class DeprecatedIPError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "A record points to deprecated IP address"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class InvalidARecordError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Should not be an A record"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class InvalidCNAMEError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "CNAME does not point to GitHub Pages"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class InvalidDNSError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Domain's DNS record could not be retrieved"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class InvalidDomainError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Domain is not a valid domain"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class InvalidRepositoryError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Repository is not a valid repository"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class MissingAccessTokenError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Cannot retrieve repository information with a valid access token"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module GitHubPages
2
+ module HealthCheck
3
+ module Errors
4
+ class NotServedByPagesError < GitHubPages::HealthCheck::Error
5
+ def message
6
+ "Domain does not resolve to the GitHub Pages server"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end