github-pages-health-check 0.6.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/README.md +83 -0
- data/github-pages-health-check.gemspec +28 -0
- data/lib/github-pages-health-check.rb +18 -245
- data/lib/github-pages-health-check/checkable.rb +62 -0
- data/lib/github-pages-health-check/cloudflare.rb +11 -7
- data/lib/github-pages-health-check/domain.rb +260 -0
- data/lib/github-pages-health-check/error.rb +7 -6
- data/lib/github-pages-health-check/errors.rb +13 -0
- data/lib/github-pages-health-check/errors/build_error.rb +8 -0
- data/lib/github-pages-health-check/errors/deprecated_ip_error.rb +11 -0
- data/lib/github-pages-health-check/errors/invalid_a_record_error.rb +11 -0
- data/lib/github-pages-health-check/errors/invalid_cname_error.rb +11 -0
- data/lib/github-pages-health-check/errors/invalid_dns_error.rb +11 -0
- data/lib/github-pages-health-check/errors/invalid_domain_error.rb +11 -0
- data/lib/github-pages-health-check/errors/invalid_repository_error.rb +11 -0
- data/lib/github-pages-health-check/errors/missing_access_token_error.rb +11 -0
- data/lib/github-pages-health-check/errors/not_served_by_pages_error.rb +11 -0
- data/lib/github-pages-health-check/printer.rb +71 -0
- data/lib/github-pages-health-check/repository.rb +74 -0
- data/lib/github-pages-health-check/site.rb +32 -0
- data/lib/github-pages-health-check/version.rb +3 -3
- data/script/bootstrap +5 -0
- data/script/check +11 -0
- data/script/check-cloudflare-ips +17 -0
- data/script/cibuild +9 -0
- data/script/console +5 -0
- data/script/release +42 -0
- data/script/test +2 -0
- data/script/update-cloudflare-ips +14 -0
- metadata +60 -7
- data/lib/github-pages-health-check/errors/deprecated_ip.rb +0 -9
- data/lib/github-pages-health-check/errors/invalid_a_record.rb +0 -9
- data/lib/github-pages-health-check/errors/invalid_cname.rb +0 -9
- data/lib/github-pages-health-check/errors/invalid_dns.rb +0 -9
- data/lib/github-pages-health-check/errors/not_served_by_pages.rb +0 -9
@@ -1,9 +1,10 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module GitHubPages
|
2
|
+
module HealthCheck
|
3
3
|
class CloudFlare
|
4
4
|
include Singleton
|
5
5
|
|
6
|
-
|
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) {
|
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
|
-
|
2
|
-
|
1
|
+
module GitHubPages
|
2
|
+
module HealthCheck
|
3
3
|
class Error < StandardError
|
4
|
-
def
|
5
|
-
|
4
|
+
def self.inherited(base)
|
5
|
+
subclasses << base
|
6
6
|
end
|
7
|
-
|
8
|
-
|
7
|
+
|
8
|
+
def self.subclasses
|
9
|
+
@subclasses ||= []
|
9
10
|
end
|
10
11
|
end
|
11
12
|
end
|