rq4 0.9.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 215a35a2ada0834699c4c8fe892b012af67cdf2156913c165e24ab196d3db4b3
4
+ data.tar.gz: 8fd6357e10dc97e10ebab775b04001a6fecae63bf7207101f207afe1de3ba278
5
+ SHA512:
6
+ metadata.gz: '02867ce8325521e5cd87f4333e287c8c388ad735fd7e92a68923ab75e41ec96cd689739da9e9dba81a03359be10dd2957bf06438abbd1664975f9477234c15df'
7
+ data.tar.gz: cc031500a1308f7cf92c6965fd7fcd058a760de6ef37c95c47d1a17cfbd949c6a19cc3ee68faffa7feb1e2cc2f58fad0dee342dc934f808aca26773e958312c2
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 J.J. Green
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ RQ4-Ruby
2
+ --------
3
+
4
+ Rack middleware adding [RQ4](https://rq4.dev) (request context)
5
+ headers to requests.
6
+
7
+ This code is beta, use with caution.
8
+
9
+ ### Rails install
10
+
11
+ Add
12
+
13
+ ```ruby
14
+ gem 'rq4'
15
+ ```
16
+
17
+ to your `Gemfile` and run `bundle install rq4`.
18
+
19
+ ### Usage
20
+
21
+ In any controller, one can access the value of the header with
22
+
23
+ ```ruby
24
+ request.env['rq4']
25
+ ```
26
+
27
+ In case of error this can be the string `error`, that would be
28
+ a bug -- please report!
29
+
30
+ ### Configuration
31
+
32
+ Optionally, add a new file
33
+ ```ruby
34
+ # config/initializers/rq4.rb
35
+ Rails.application.config.tap do |config|
36
+ config.rq4 = ActiveSupport::OrderedOptions.new
37
+ config.rq4.prefetch_skip = true
38
+ config.rq4.prefetch_key = 'x-sec-purpose'
39
+ config.rq4.prefetch_value = 'prefetch'
40
+ end
41
+ ```
42
+ The options are to handle the prefetching of links typically
43
+ used by Turbo: those are mistaken for bot requests (typically
44
+ with a `vv-x`) by the default configuration. But one can skip
45
+ the relevant test entirely with `prefetch_skip` set `true`.
46
+ Alternatively, since Turbo adds the header
47
+ ```
48
+ x-turbo-request-id: f90cf13d-0...
49
+ ```
50
+ one can use
51
+ ```ruby
52
+ config.rq4.prefetch_key = 'x-turbo-request-id'
53
+ ```
54
+ to skip if that key is present, or
55
+ ```ruby
56
+ config.rq4.prefetch_key = 'x-sec-purpose'
57
+ config.rq4.prefetch_value = 'prefetch'
58
+ ```
59
+ to skip if the key _and_ specified value are present. The
60
+ former is more reliable since the `x-sec-purpose` is only
61
+ added to `GET` requests.
@@ -0,0 +1,8 @@
1
+ module RQ4
2
+ class Railtie < Rails::Railtie
3
+ initializer 'rq4.insert_middleware' do |app|
4
+ config = Rails.application.config
5
+ app.middleware.use RQ4::Middleware, config
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module RQ4
2
+ VERSION = '0.9.0'
3
+ end
data/lib/rq4.rb ADDED
@@ -0,0 +1,429 @@
1
+ require 'rq4/railtie' if defined?(Rails::Railtie)
2
+ require 'yaml'
3
+ require 'set'
4
+
5
+ module RQ4 ; end
6
+
7
+ class RQ4::Middleware
8
+ private attr_reader :request, :config
9
+
10
+ def initialize(app, config = nil)
11
+ @app = app
12
+ @config = config
13
+ end
14
+
15
+ def call(env)
16
+ @request = Rack::Request.new(env)
17
+ @request.set_header('rq4', rq4)
18
+ @app.call(env)
19
+ end
20
+
21
+ private
22
+
23
+ def options
24
+ if config.respond_to? :rq4 then
25
+ config.rq4
26
+ end
27
+ end
28
+
29
+ def rq4
30
+ [Mode, Upgrade, Identity, Transfer]
31
+ .map { |cls| cls.new(request, options:).code }
32
+ .join
33
+ rescue StandardError => e
34
+ request.set_header('rack.errors', e.message)
35
+ 'error'
36
+ end
37
+
38
+ class Base
39
+ CODE = {
40
+ valid: 'v',
41
+ invalid: 'x',
42
+ indeterminate: '-'
43
+ }.freeze
44
+
45
+ private attr_reader :request, :options
46
+
47
+ def initialize(request, options:)
48
+ @request = request
49
+ @options = options
50
+ end
51
+
52
+ def status
53
+ raise PureVirtualError
54
+ end
55
+
56
+ def code
57
+ CODE.fetch(status)
58
+ end
59
+
60
+ private
61
+
62
+ # Rails can be configured with link prefetching (with Turbo
63
+ # for example), and those will trigger the accept_on_cors test
64
+ # so giving a false positive.
65
+
66
+ def prefetch_skip?
67
+ options&.prefetch_skip || false
68
+ end
69
+
70
+ def prefetch_key
71
+ options&.prefetch_key
72
+ end
73
+
74
+ def prefetch_value
75
+ options&.prefetch_value
76
+ end
77
+
78
+ def prefetch_rack_key
79
+ ['HTTP', *prefetch_key.split('-')].join('_').upcase
80
+ end
81
+
82
+ def prefetch_header?
83
+ if prefetch_key then
84
+ value = request.get_header(prefetch_rack_key)
85
+ if prefetch_value then
86
+ value == prefetch_value
87
+ else
88
+ ! value.nil?
89
+ end
90
+ else
91
+ false
92
+ end
93
+ end
94
+
95
+ # standard headers
96
+
97
+ def content_type
98
+ request.get_header('CONTENT_TYPE')
99
+ end
100
+
101
+ def content_length
102
+ request.get_header('CONTENT_LENGTH')
103
+ end
104
+
105
+ def mode
106
+ request.get_header('HTTP_SEC_FETCH_MODE')
107
+ end
108
+
109
+ def dest
110
+ request.get_header('HTTP_SEC_FETCH_DEST')
111
+ end
112
+
113
+ def site
114
+ request.get_header('HTTP_SEC_FETCH_SITE')
115
+ end
116
+
117
+ def user
118
+ request.get_header('HTTP_SEC_FETCH_USER')
119
+ end
120
+
121
+ def uir
122
+ request.get_header('HTTP_UPGRADE_INSECURE_REQUESTS')
123
+ end
124
+
125
+ def ua
126
+ request.get_header('HTTP_USER_AGENT')
127
+ end
128
+
129
+ def sec_ch_ua
130
+ request.get_header('HTTP_SEC_CH_UA')
131
+ end
132
+
133
+ def sec_ch_ua_platform
134
+ request.get_header('HTTP_SEC_CH_UA_PLATFORM')
135
+ end
136
+
137
+ def sec_ch_ua_mobile
138
+ request.get_header('HTTP_SEC_CH_UA_MOBILE')
139
+ end
140
+
141
+ def accept
142
+ request.get_header('HTTP_ACCEPT')
143
+ end
144
+
145
+ def accept_language
146
+ request.get_header('HTTP_ACCEPT_LANGUAGE')
147
+ end
148
+ end
149
+
150
+ class Mode < Base
151
+ VALID = YAML.load(
152
+ File.read(__FILE__).split("__END__\n", 2).last
153
+ ).map { |k, v| [k, Set.new(v)] }.to_h.freeze
154
+
155
+ def status
156
+ @status ||= status_eval
157
+ end
158
+
159
+ private
160
+
161
+ def status_eval
162
+ if mode then
163
+ if dest then
164
+ if (mode == 'navigate') && (dest == 'empty') then
165
+ :invalid
166
+ elsif VALID.fetch(mode, []).include? dest then
167
+ :valid
168
+ else
169
+ :invalid
170
+ end
171
+ else
172
+ :invalid
173
+ end
174
+ else
175
+ :indeterminate
176
+ end
177
+ end
178
+ end
179
+
180
+ class Upgrade < Base
181
+
182
+ def status
183
+ @status ||= status_eval
184
+ end
185
+
186
+ private
187
+
188
+ def status_eval
189
+ if mode then
190
+ if uir.nil? && user.nil? then
191
+ :valid
192
+ elsif mode == 'navigate' then
193
+ :valid
194
+ elsif (uir == '1') || (user == '?1') then
195
+ :invalid
196
+ else
197
+ :valid
198
+ end
199
+ else
200
+ :indeterminate
201
+ end
202
+ end
203
+ end
204
+
205
+ class Identity < Base
206
+
207
+ def status
208
+ @status ||= status_eval
209
+ end
210
+
211
+ private
212
+
213
+ def status_eval
214
+ if sec_ch_ua || sec_ch_ua_platform || sec_ch_ua_mobile then
215
+ if ua_consistent? &&
216
+ platform_consistent? &&
217
+ mobile_consistent? then
218
+ :valid
219
+ else
220
+ :invalid
221
+ end
222
+ else
223
+ :indeterminate
224
+ end
225
+ end
226
+
227
+ def ua_lc
228
+ @ua_lc ||= ua.downcase
229
+ end
230
+
231
+ def ua_firefox?
232
+ ua_lc.include?('firefox/')
233
+ end
234
+
235
+ def ua_safari?
236
+ ua_lc.include?('safari/')
237
+ end
238
+
239
+ def ua_chrome?
240
+ ua_lc.include?('chrome/')
241
+ end
242
+
243
+ def ua_chromium?
244
+ ua_lc.include?('chromium/')
245
+ end
246
+
247
+ def ua_consistent?
248
+ if sec_ch_ua then
249
+ if ua_firefox? && !ua_chrome? then
250
+ false
251
+ elsif ua_safari? && !ua_chrome? && !ua_chromium? then
252
+ false
253
+ else
254
+ true
255
+ end
256
+ else
257
+ true
258
+ end
259
+ end
260
+
261
+ def platform
262
+ @platform ||= sec_ch_ua_platform.delete('"').downcase
263
+ end
264
+
265
+ def platform_windows?
266
+ platform == 'windows'
267
+ end
268
+
269
+ def platform_macos?
270
+ platform == 'macos'
271
+ end
272
+
273
+ def platform_linux?
274
+ platform == 'linux'
275
+ end
276
+
277
+ def ua_macintosh?
278
+ ua_lc.include? 'macintosh'
279
+ end
280
+
281
+ def ua_windows_nt?
282
+ ua_lc.include? 'windows nt'
283
+ end
284
+
285
+ def platform_consistent?
286
+ if sec_ch_ua_platform && ua then
287
+ if platform_windows? && ua_macintosh? then
288
+ false
289
+ elsif platform_macos? && ua_windows_nt? then
290
+ false
291
+ elsif platform_linux? && (ua_macintosh? || ua_windows_nt?) then
292
+ false
293
+ else
294
+ true
295
+ end
296
+ else
297
+ true
298
+ end
299
+ end
300
+
301
+ def mobile?
302
+ sec_ch_ua_mobile == '?1'
303
+ end
304
+
305
+ def ua_mobile?
306
+ ['mobile', 'android'].any? { |name| ua_lc.include?(name) }
307
+ end
308
+
309
+ def ua_desktop?
310
+ (ua_macintosh? || ua_windows_nt?) && !ua_mobile?
311
+ end
312
+
313
+ def all_ua_hints?
314
+ sec_ch_ua && sec_ch_ua_platform && sec_ch_ua_mobile
315
+ end
316
+
317
+ def no_accept_language?
318
+ ! accept_language
319
+ end
320
+
321
+ def mobile_consistent?
322
+ if sec_ch_ua_mobile && ua then
323
+ if mobile? && ua_desktop? then
324
+ false
325
+ elsif all_ua_hints? && no_accept_language? then
326
+ false
327
+ else
328
+ true
329
+ end
330
+ else
331
+ true
332
+ end
333
+ end
334
+ end
335
+
336
+ class Transfer < Base
337
+
338
+ def status
339
+ @status ||= status_eval
340
+ end
341
+
342
+ private
343
+
344
+ def status_eval
345
+ if mode || content_type then
346
+ if empty_form_urlencoded_site_none? then
347
+ :invalid
348
+ elsif accept_on_cors? then
349
+ :invalid
350
+ else
351
+ :valid
352
+ end
353
+ else
354
+ :indeterminate
355
+ end
356
+ end
357
+
358
+ def empty_form_urlencoded_site_none?
359
+ content_type &&
360
+ content_type.include?('application/x-www-form-urlencoded') &&
361
+ content_length == '0' &&
362
+ site == 'none'
363
+ end
364
+
365
+ def accept_on_cors?
366
+ if prefetch_skip? || prefetch_header? then
367
+ false
368
+ else
369
+ accept &&
370
+ accept.include?('text/html') &&
371
+ mode == 'cors'
372
+ end
373
+ end
374
+ end
375
+ end
376
+
377
+ __END__
378
+ # -*- yaml -*-
379
+ # valid Sec-Fetch-Dest values
380
+ ---
381
+ navigate:
382
+ - document
383
+ - embed
384
+ - frame
385
+ - iframe
386
+ - object
387
+ same-origin:
388
+ - audio
389
+ - empty
390
+ - font
391
+ - image
392
+ - json
393
+ - manifest
394
+ - report
395
+ - script
396
+ - sharedworker
397
+ - style
398
+ - track
399
+ - video
400
+ - worker
401
+ - xslt
402
+ cors:
403
+ - audio
404
+ - empty
405
+ - font
406
+ - image
407
+ - json
408
+ - manifest
409
+ - report
410
+ - script
411
+ - sharedworker
412
+ - style
413
+ - track
414
+ - video
415
+ - xslt
416
+ no-cors:
417
+ - audio
418
+ - embed
419
+ - empty
420
+ - image
421
+ - object
422
+ - report
423
+ - script
424
+ - style
425
+ - track
426
+ - video
427
+ websocket:
428
+ - empty
429
+ - websocket
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rq4
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - J.J. Green
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rack
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: simplecov-cobertura
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: yard
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubygems-tasks
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rbs
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ description: 'Rack middleware implementing the RQ4 fingerprint
139
+
140
+ '
141
+ email: j.j.green@gmx.co.uk
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - LICENCE
147
+ - README.md
148
+ - lib/rq4.rb
149
+ - lib/rq4/railtie.rb
150
+ - lib/rq4/version.rb
151
+ homepage: https://gitlab.com/jjg/rq4-ruby
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '3.2'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 4.0.16
170
+ specification_version: 4
171
+ summary: RQ4 request fingerprint
172
+ test_files: []