browserx 1.0.0 → 2.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/CHANGELOG.md +39 -1
- data/README.md +122 -21
- data/bot_categories.yml +1522 -0
- data/bot_ip_ranges.yml +3035 -0
- data/bots.yml +1249 -0
- data/lib/browser/action_controller.rb +2 -1
- data/lib/browser/base.rb +20 -5
- data/lib/browser/bot/ip_ranges.rb +194 -0
- data/lib/browser/bot/known_bots_matcher.rb +1 -1
- data/lib/browser/bot.rb +98 -2
- data/lib/browser/browser.rb +69 -1
- data/lib/browser/cache.rb +92 -0
- data/lib/browser/client_hints.rb +73 -0
- data/lib/browser/cloudflare.rb +65 -0
- data/lib/browser/detection.rb +25 -0
- data/lib/browser/device/psvita.rb +1 -1
- data/lib/browser/device/surface.rb +1 -1
- data/lib/browser/device.rb +31 -7
- data/lib/browser/middleware/context.rb +2 -1
- data/lib/browser/platform.rb +34 -7
- data/lib/browser/version.rb +1 -1
- data/lib/browserx.rb +1 -1
- metadata +11 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browser
|
|
4
|
+
# What Cloudflare knows of the request, from the headers a Transform Rule
|
|
5
|
+
# sets (docs/guide.html#cloudflare) and its own CF-IPCountry. Each site names
|
|
6
|
+
# its headers in its rule: Browser::Cloudflare.header_names= changes them.
|
|
7
|
+
class Cloudflare
|
|
8
|
+
DEFAULT_HEADER_NAMES = {
|
|
9
|
+
verified_bot: "X-Bot-Verified",
|
|
10
|
+
bot_category: "X-Bot-Category",
|
|
11
|
+
bot_score: "X-Bot-Score",
|
|
12
|
+
country: "CF-IPCountry"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_reader :header_names, :env_key_by_signal
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Merged into the defaults: {bot_score: "X-Edge-Bot-Score"}.
|
|
20
|
+
def self.header_names=(names)
|
|
21
|
+
unknown_signals = names.keys - DEFAULT_HEADER_NAMES.keys
|
|
22
|
+
raise ArgumentError, "unknown Cloudflare signals: #{unknown_signals.inspect}" if unknown_signals.any?
|
|
23
|
+
|
|
24
|
+
@header_names = DEFAULT_HEADER_NAMES.merge(names).freeze
|
|
25
|
+
@env_key_by_signal = @header_names.transform_values { |name| "HTTP_#{name.upcase.tr("-", "_")}" }.freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self.header_names = {}
|
|
29
|
+
|
|
30
|
+
def initialize(headers)
|
|
31
|
+
@headers = headers
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# cf.bot_management.verified_bot (cf.client.bot without Bot Management).
|
|
35
|
+
def verified_bot?
|
|
36
|
+
header(:verified_bot)&.casecmp?("true") || false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# cf.verified_bot_category, in bot.category's vocabulary: "Monitoring &
|
|
40
|
+
# Analytics" is :monitoring_and_analytics.
|
|
41
|
+
def bot_category
|
|
42
|
+
category = header(:bot_category)&.strip
|
|
43
|
+
return if category.nil? || category.empty?
|
|
44
|
+
|
|
45
|
+
category.downcase.gsub("&", "and").gsub(/[^a-z0-9]+/, "_").to_sym
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# cf.bot_management.score, Bot Management only: 1 is a bot, 99 a human.
|
|
49
|
+
def bot_score
|
|
50
|
+
score = header(:bot_score)&.strip
|
|
51
|
+
score.to_i if score&.match?(/\A\d{1,2}\z/)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The ISO 3166-1 alpha-2 country, "XX" when unknown, "T1" for Tor.
|
|
55
|
+
def country
|
|
56
|
+
country = header(:country)&.strip
|
|
57
|
+
country if country&.match?(/\A[A-Z0-9]{2}\z/)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private def header(signal)
|
|
61
|
+
value = @headers&.[](self.class.env_key_by_signal[signal])
|
|
62
|
+
value if value.is_a?(String)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browser
|
|
4
|
+
# What a user agent is detected as, shared by every browser built from it
|
|
5
|
+
# while it stays in the cache. Each answer is computed on first use.
|
|
6
|
+
class Detection
|
|
7
|
+
attr_accessor :browser_class
|
|
8
|
+
|
|
9
|
+
def initialize(ua)
|
|
10
|
+
@ua = ua
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def platform
|
|
14
|
+
@platform ||= Platform.new(@ua)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def device
|
|
18
|
+
@device ||= Device.new(@ua, platform:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def bot
|
|
22
|
+
@bot ||= Bot.new(@ua)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/browser/device.rb
CHANGED
|
@@ -53,9 +53,19 @@ module Browser
|
|
|
53
53
|
]
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def initialize(ua, platform: nil)
|
|
56
|
+
def initialize(ua, platform: nil, client_hints: nil)
|
|
57
57
|
@ua = ua
|
|
58
58
|
@platform = platform
|
|
59
|
+
@client_hints = client_hints
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# This device, or one built with the client hints when Sec-CH-UA-Mobile or
|
|
63
|
+
# the platform they gave change its answers.
|
|
64
|
+
def with_client_hints(client_hints, platform:)
|
|
65
|
+
mobile = client_hints.mobile?
|
|
66
|
+
return self if platform.equal?(self.platform) && (mobile.nil? || mobile == detect_mobile?)
|
|
67
|
+
|
|
68
|
+
self.class.new(ua, platform:, client_hints:)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
def subject
|
|
@@ -201,15 +211,29 @@ module Browser
|
|
|
201
211
|
end
|
|
202
212
|
|
|
203
213
|
# Regex taken from http://detectmobilebrowsers.com
|
|
204
|
-
|
|
214
|
+
MOBILE_REGEX = %r{(android|bb\d+|meego).+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino}i
|
|
215
|
+
MOBILE_PREFIX_REGEX = %r{1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-}i
|
|
216
|
+
# Both are lowercase: on an ASCII user agent, matching its downcased copy
|
|
217
|
+
# without /i gives the same answer several times faster. Every prefix is 4
|
|
218
|
+
# characters long, so anchoring it tests the first 4 without copying them.
|
|
219
|
+
MOBILE_LOWERCASE_REGEX = Regexp.new(MOBILE_REGEX.source)
|
|
220
|
+
MOBILE_PREFIX_LOWERCASE_REGEX = Regexp.new("\\A(?:#{MOBILE_PREFIX_REGEX.source})")
|
|
221
|
+
private_constant :MOBILE_REGEX, :MOBILE_PREFIX_REGEX, :MOBILE_LOWERCASE_REGEX,
|
|
222
|
+
:MOBILE_PREFIX_LOWERCASE_REGEX
|
|
223
|
+
|
|
205
224
|
private def detect_mobile?
|
|
206
225
|
return @detect_mobile if defined?(@detect_mobile)
|
|
207
226
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
227
|
+
mobile = @client_hints&.mobile?
|
|
228
|
+
@detect_mobile = mobile.nil? ? psp? || /zunewp7|crios/i.match?(ua) || mobile_user_agent? : mobile
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private def mobile_user_agent?
|
|
232
|
+
return MOBILE_REGEX.match?(ua) || MOBILE_PREFIX_REGEX.match?(ua[0..3]) unless ua.ascii_only?
|
|
233
|
+
|
|
234
|
+
lowercase_ua = ua.downcase
|
|
235
|
+
MOBILE_LOWERCASE_REGEX.match?(lowercase_ua) ||
|
|
236
|
+
MOBILE_PREFIX_LOWERCASE_REGEX.match?(lowercase_ua)
|
|
213
237
|
end
|
|
214
238
|
|
|
215
239
|
private def platform
|
data/lib/browser/platform.rb
CHANGED
|
@@ -41,15 +41,23 @@ module Browser
|
|
|
41
41
|
]
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def initialize(ua)
|
|
44
|
+
def initialize(ua, client_hints: nil)
|
|
45
45
|
@ua = ua
|
|
46
|
+
@client_hints = client_hints
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# This platform, or one built with the client hints when they tell more
|
|
50
|
+
# than the user agent: the cached platform of a user agent ignores them.
|
|
51
|
+
def with_client_hints(client_hints)
|
|
52
|
+
hinted_class = client_hints.platform_class
|
|
53
|
+
return self unless hinted_class
|
|
54
|
+
return self if subject.instance_of?(hinted_class) && client_hints.platform_version.nil?
|
|
55
|
+
|
|
56
|
+
self.class.new(ua, client_hints:)
|
|
46
57
|
end
|
|
47
58
|
|
|
48
59
|
def subject
|
|
49
|
-
@subject ||= self
|
|
50
|
-
candidate = matcher.new(ua, self)
|
|
51
|
-
break candidate if candidate.match?
|
|
52
|
-
end
|
|
60
|
+
@subject ||= @client_hints&.platform_class&.new(ua, self) || detect_subject
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def adobe_air?(expected_version = nil)
|
|
@@ -109,7 +117,7 @@ module Browser
|
|
|
109
117
|
end
|
|
110
118
|
|
|
111
119
|
def version
|
|
112
|
-
subject.version
|
|
120
|
+
@version ||= hinted_version || subject.version
|
|
113
121
|
end
|
|
114
122
|
|
|
115
123
|
def name
|
|
@@ -163,7 +171,12 @@ module Browser
|
|
|
163
171
|
end
|
|
164
172
|
|
|
165
173
|
def windows10?
|
|
166
|
-
windows? && ua.include?("Windows NT 10")
|
|
174
|
+
windows? && ua.include?("Windows NT 10") && !windows11?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Only the client hints tell Windows 11 apart (ClientHints#windows11?).
|
|
178
|
+
def windows11?
|
|
179
|
+
windows? && version == "11"
|
|
167
180
|
end
|
|
168
181
|
|
|
169
182
|
def windows_rt?
|
|
@@ -187,6 +200,20 @@ module Browser
|
|
|
187
200
|
windows? && ua.include?("Touch")
|
|
188
201
|
end
|
|
189
202
|
|
|
203
|
+
private def detect_subject
|
|
204
|
+
self.class.matchers.each do |matcher|
|
|
205
|
+
candidate = matcher.new(ua, self)
|
|
206
|
+
break candidate if candidate.match?
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private def hinted_version
|
|
211
|
+
return unless @client_hints&.platform_class
|
|
212
|
+
return @client_hints.platform_version unless id == :windows
|
|
213
|
+
|
|
214
|
+
"11" if @client_hints.windows11?
|
|
215
|
+
end
|
|
216
|
+
|
|
190
217
|
private :subject
|
|
191
218
|
private :ua
|
|
192
219
|
end
|
data/lib/browser/version.rb
CHANGED
data/lib/browserx.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nando Vieira
|
|
@@ -114,14 +114,14 @@ dependencies:
|
|
|
114
114
|
requirements:
|
|
115
115
|
- - ">="
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0'
|
|
117
|
+
version: '1.0'
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - ">="
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0'
|
|
124
|
+
version: '1.0'
|
|
125
125
|
description: 'A drop-in replacement for the browser gem: the same Browser API and
|
|
126
126
|
require paths, answering nearly twice as fast with half the allocations.'
|
|
127
127
|
executables: []
|
|
@@ -131,7 +131,9 @@ files:
|
|
|
131
131
|
- CHANGELOG.md
|
|
132
132
|
- LICENSE.md
|
|
133
133
|
- README.md
|
|
134
|
+
- bot_categories.yml
|
|
134
135
|
- bot_exceptions.yml
|
|
136
|
+
- bot_ip_ranges.yml
|
|
135
137
|
- bots.yml
|
|
136
138
|
- languages.yml
|
|
137
139
|
- lib/browser.rb
|
|
@@ -143,11 +145,16 @@ files:
|
|
|
143
145
|
- lib/browser/blackberry.rb
|
|
144
146
|
- lib/browser/bot.rb
|
|
145
147
|
- lib/browser/bot/empty_user_agent_matcher.rb
|
|
148
|
+
- lib/browser/bot/ip_ranges.rb
|
|
146
149
|
- lib/browser/bot/keyword_matcher.rb
|
|
147
150
|
- lib/browser/bot/known_bots_matcher.rb
|
|
148
151
|
- lib/browser/browser.rb
|
|
152
|
+
- lib/browser/cache.rb
|
|
149
153
|
- lib/browser/chrome.rb
|
|
154
|
+
- lib/browser/client_hints.rb
|
|
155
|
+
- lib/browser/cloudflare.rb
|
|
150
156
|
- lib/browser/detect_version.rb
|
|
157
|
+
- lib/browser/detection.rb
|
|
151
158
|
- lib/browser/device.rb
|
|
152
159
|
- lib/browser/device/android.rb
|
|
153
160
|
- lib/browser/device/base.rb
|
|
@@ -261,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
261
268
|
- !ruby/object:Gem::Version
|
|
262
269
|
version: '0'
|
|
263
270
|
requirements: []
|
|
264
|
-
rubygems_version: 4.0.
|
|
271
|
+
rubygems_version: 4.0.20
|
|
265
272
|
specification_version: 4
|
|
266
273
|
summary: 'Browser detection for Ruby: the browser gem''s API, faster'
|
|
267
274
|
test_files: []
|