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.
@@ -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
@@ -12,7 +12,7 @@ module Browser
12
12
  end
13
13
 
14
14
  def match?
15
- ua.match?("Playstation Vita")
15
+ ua.include?("Playstation Vita")
16
16
  end
17
17
  end
18
18
  end
@@ -12,7 +12,7 @@ module Browser
12
12
  end
13
13
 
14
14
  def match?
15
- platform.windows_rt? && ua.include?("Touch")
15
+ ua.include?("Touch") && platform.windows_rt?
16
16
  end
17
17
 
18
18
  private def platform
@@ -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
- # rubocop:disable-next Layout/LineLength
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
- @detect_mobile =
209
- psp? ||
210
- /zunewp7|crios/i.match?(ua) ||
211
- %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.match?(ua) ||
212
- %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.match?(ua[0..3])
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
@@ -10,7 +10,8 @@ module Browser
10
10
 
11
11
  @browser = Browser.new(
12
12
  request.user_agent,
13
- accept_language: request.env["HTTP_ACCEPT_LANGUAGE"]
13
+ accept_language: request.env["HTTP_ACCEPT_LANGUAGE"],
14
+ headers: request.env
14
15
  )
15
16
  end
16
17
 
@@ -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.class.matchers.each do |matcher|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browser
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/browserx.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "browser"
3
+ require_relative "browser"
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: 1.0.0
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.23
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: []