sidekiq 5.2.10 → 6.5.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +391 -1
  3. data/LICENSE +3 -3
  4. data/README.md +24 -35
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +79 -67
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +504 -307
  13. data/lib/sidekiq/cli.rb +190 -206
  14. data/lib/sidekiq/client.rb +77 -81
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +8 -7
  17. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +7 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +33 -7
  24. data/lib/sidekiq/job_retry.rb +126 -106
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +177 -83
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +40 -41
  29. data/lib/sidekiq/metrics/deploy.rb +47 -0
  30. data/lib/sidekiq/metrics/query.rb +153 -0
  31. data/lib/sidekiq/metrics/shared.rb +94 -0
  32. data/lib/sidekiq/metrics/tracking.rb +134 -0
  33. data/lib/sidekiq/middleware/chain.rb +102 -46
  34. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  35. data/lib/sidekiq/middleware/i18n.rb +7 -7
  36. data/lib/sidekiq/middleware/modules.rb +21 -0
  37. data/lib/sidekiq/monitor.rb +133 -0
  38. data/lib/sidekiq/paginator.rb +20 -16
  39. data/lib/sidekiq/processor.rb +104 -97
  40. data/lib/sidekiq/rails.rb +47 -37
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +108 -77
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +64 -35
  45. data/lib/sidekiq/sd_notify.rb +149 -0
  46. data/lib/sidekiq/systemd.rb +24 -0
  47. data/lib/sidekiq/testing/inline.rb +6 -5
  48. data/lib/sidekiq/testing.rb +68 -58
  49. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web/action.rb +15 -11
  52. data/lib/sidekiq/web/application.rb +100 -77
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +134 -94
  55. data/lib/sidekiq/web/router.rb +23 -19
  56. data/lib/sidekiq/web.rb +65 -105
  57. data/lib/sidekiq/worker.rb +253 -106
  58. data/lib/sidekiq.rb +170 -62
  59. data/sidekiq.gemspec +23 -16
  60. data/web/assets/images/apple-touch-icon.png +0 -0
  61. data/web/assets/javascripts/application.js +112 -61
  62. data/web/assets/javascripts/chart.min.js +13 -0
  63. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  64. data/web/assets/javascripts/dashboard.js +53 -89
  65. data/web/assets/javascripts/graph.js +16 -0
  66. data/web/assets/javascripts/metrics.js +262 -0
  67. data/web/assets/stylesheets/application-dark.css +143 -0
  68. data/web/assets/stylesheets/application-rtl.css +0 -4
  69. data/web/assets/stylesheets/application.css +88 -233
  70. data/web/locales/ar.yml +8 -2
  71. data/web/locales/de.yml +14 -2
  72. data/web/locales/el.yml +43 -19
  73. data/web/locales/en.yml +13 -1
  74. data/web/locales/es.yml +18 -2
  75. data/web/locales/fr.yml +10 -3
  76. data/web/locales/ja.yml +7 -1
  77. data/web/locales/lt.yml +83 -0
  78. data/web/locales/pl.yml +4 -4
  79. data/web/locales/pt-br.yml +27 -9
  80. data/web/locales/ru.yml +4 -0
  81. data/web/locales/vi.yml +83 -0
  82. data/web/views/_footer.erb +1 -1
  83. data/web/views/_job_info.erb +3 -2
  84. data/web/views/_nav.erb +1 -1
  85. data/web/views/_poll_link.erb +2 -5
  86. data/web/views/_summary.erb +7 -7
  87. data/web/views/busy.erb +56 -22
  88. data/web/views/dashboard.erb +23 -14
  89. data/web/views/dead.erb +3 -3
  90. data/web/views/layout.erb +3 -1
  91. data/web/views/metrics.erb +69 -0
  92. data/web/views/metrics_for_job.erb +87 -0
  93. data/web/views/morgue.erb +9 -6
  94. data/web/views/queue.erb +23 -10
  95. data/web/views/queues.erb +10 -2
  96. data/web/views/retries.erb +11 -8
  97. data/web/views/retry.erb +3 -3
  98. data/web/views/scheduled.erb +5 -2
  99. metadata +53 -64
  100. data/.circleci/config.yml +0 -61
  101. data/.github/contributing.md +0 -32
  102. data/.github/issue_template.md +0 -11
  103. data/.gitignore +0 -15
  104. data/.travis.yml +0 -11
  105. data/3.0-Upgrade.md +0 -70
  106. data/4.0-Upgrade.md +0 -53
  107. data/5.0-Upgrade.md +0 -56
  108. data/COMM-LICENSE +0 -97
  109. data/Ent-Changes.md +0 -238
  110. data/Gemfile +0 -19
  111. data/Pro-2.0-Upgrade.md +0 -138
  112. data/Pro-3.0-Upgrade.md +0 -44
  113. data/Pro-4.0-Upgrade.md +0 -35
  114. data/Pro-Changes.md +0 -759
  115. data/Rakefile +0 -9
  116. data/bin/sidekiqctl +0 -20
  117. data/code_of_conduct.md +0 -50
  118. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  119. data/lib/sidekiq/core_ext.rb +0 -1
  120. data/lib/sidekiq/ctl.rb +0 -221
  121. data/lib/sidekiq/exception_handler.rb +0 -29
  122. data/lib/sidekiq/logging.rb +0 -122
  123. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  124. data/lib/sidekiq/util.rb +0 -66
@@ -4,9 +4,7 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length"
8
- CONTENT_TYPE = "Content-Type"
9
- REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
7
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
10
8
  CSP_HEADER = [
11
9
  "default-src 'self' https: http:",
12
10
  "child-src 'self'",
@@ -21,7 +19,7 @@ module Sidekiq
21
19
  "style-src 'self' https: http: 'unsafe-inline'",
22
20
  "worker-src 'self'",
23
21
  "base-uri 'self'"
24
- ].join('; ').freeze
22
+ ].join("; ").freeze
25
23
 
26
24
  def initialize(klass)
27
25
  @klass = klass
@@ -43,8 +41,15 @@ module Sidekiq
43
41
  # nothing, backwards compatibility
44
42
  end
45
43
 
44
+ head "/" do
45
+ # HEAD / is the cheapest heartbeat possible,
46
+ # it hits Redis to ensure connectivity
47
+ Sidekiq.redis { |c| c.llen("queue:default") }
48
+ ""
49
+ end
50
+
46
51
  get "/" do
47
- @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
52
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
48
53
  days = (params["days"] || 30).to_i
49
54
  return halt(401) if days < 1 || days > 180
50
55
 
@@ -55,19 +60,32 @@ module Sidekiq
55
60
  erb(:dashboard)
56
61
  end
57
62
 
63
+ get "/metrics" do
64
+ q = Sidekiq::Metrics::Query.new
65
+ @query_result = q.top_jobs
66
+ erb(:metrics)
67
+ end
68
+
69
+ get "/metrics/:name" do
70
+ @name = route_params[:name]
71
+ q = Sidekiq::Metrics::Query.new
72
+ @query_result = q.for_job(@name)
73
+ erb(:metrics_for_job)
74
+ end
75
+
58
76
  get "/busy" do
59
77
  erb(:busy)
60
78
  end
61
79
 
62
80
  post "/busy" do
63
- if params['identity']
64
- p = Sidekiq::Process.new('identity' => params['identity'])
65
- p.quiet! if params['quiet']
66
- p.stop! if params['stop']
81
+ if params["identity"]
82
+ p = Sidekiq::Process.new("identity" => params["identity"])
83
+ p.quiet! if params["quiet"]
84
+ p.stop! if params["stop"]
67
85
  else
68
86
  processes.each do |pro|
69
- pro.quiet! if params['quiet']
70
- pro.stop! if params['stop']
87
+ pro.quiet! if params["quiet"]
88
+ pro.stop! if params["stop"]
71
89
  end
72
90
  end
73
91
 
@@ -80,42 +98,53 @@ module Sidekiq
80
98
  erb(:queues)
81
99
  end
82
100
 
101
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
102
+
83
103
  get "/queues/:name" do
84
104
  @name = route_params[:name]
85
105
 
86
- halt(404) unless @name
106
+ halt(404) if !@name || @name !~ QUEUE_NAME
87
107
 
88
- @count = (params['count'] || 25).to_i
108
+ @count = (params["count"] || 25).to_i
89
109
  @queue = Sidekiq::Queue.new(@name)
90
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
91
- @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
110
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
111
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
92
112
 
93
113
  erb(:queue)
94
114
  end
95
115
 
96
116
  post "/queues/:name" do
97
- Sidekiq::Queue.new(route_params[:name]).clear
117
+ queue = Sidekiq::Queue.new(route_params[:name])
118
+
119
+ if Sidekiq.pro? && params["pause"]
120
+ queue.pause!
121
+ elsif Sidekiq.pro? && params["unpause"]
122
+ queue.unpause!
123
+ else
124
+ queue.clear
125
+ end
98
126
 
99
127
  redirect "#{root_path}queues"
100
128
  end
101
129
 
102
130
  post "/queues/:name/delete" do
103
131
  name = route_params[:name]
104
- Sidekiq::Job.new(params['key_val'], name).delete
132
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
105
133
 
106
134
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
107
135
  end
108
136
 
109
- get '/morgue' do
110
- @count = (params['count'] || 25).to_i
111
- (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
137
+ get "/morgue" do
138
+ @count = (params["count"] || 25).to_i
139
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
112
140
  @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
113
141
 
114
142
  erb(:morgue)
115
143
  end
116
144
 
117
145
  get "/morgue/:key" do
118
- halt(404) unless key = route_params[:key]
146
+ key = route_params[:key]
147
+ halt(404) unless key
119
148
 
120
149
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
121
150
 
@@ -126,10 +155,10 @@ module Sidekiq
126
155
  end
127
156
  end
128
157
 
129
- post '/morgue' do
130
- redirect(request.path) unless params['key']
158
+ post "/morgue" do
159
+ redirect(request.path) unless params["key"]
131
160
 
132
- params['key'].each do |key|
161
+ params["key"].each do |key|
133
162
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
134
163
  retry_or_delete_or_kill job, params if job
135
164
  end
@@ -150,7 +179,8 @@ module Sidekiq
150
179
  end
151
180
 
152
181
  post "/morgue/:key" do
153
- halt(404) unless key = route_params[:key]
182
+ key = route_params[:key]
183
+ halt(404) unless key
154
184
 
155
185
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
156
186
  retry_or_delete_or_kill job, params if job
@@ -158,9 +188,9 @@ module Sidekiq
158
188
  redirect_with_query("#{root_path}morgue")
159
189
  end
160
190
 
161
- get '/retries' do
162
- @count = (params['count'] || 25).to_i
163
- (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
191
+ get "/retries" do
192
+ @count = (params["count"] || 25).to_i
193
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
164
194
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
165
195
 
166
196
  erb(:retries)
@@ -176,10 +206,10 @@ module Sidekiq
176
206
  end
177
207
  end
178
208
 
179
- post '/retries' do
180
- redirect(request.path) unless params['key']
209
+ post "/retries" do
210
+ redirect(request.path) unless params["key"]
181
211
 
182
- params['key'].each do |key|
212
+ params["key"].each do |key|
183
213
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
184
214
  retry_or_delete_or_kill job, params if job
185
215
  end
@@ -213,9 +243,9 @@ module Sidekiq
213
243
  redirect_with_query("#{root_path}retries")
214
244
  end
215
245
 
216
- get '/scheduled' do
217
- @count = (params['count'] || 25).to_i
218
- (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
246
+ get "/scheduled" do
247
+ @count = (params["count"] || 25).to_i
248
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
219
249
  @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
220
250
 
221
251
  erb(:scheduled)
@@ -231,10 +261,10 @@ module Sidekiq
231
261
  end
232
262
  end
233
263
 
234
- post '/scheduled' do
235
- redirect(request.path) unless params['key']
264
+ post "/scheduled" do
265
+ redirect(request.path) unless params["key"]
236
266
 
237
- params['key'].each do |key|
267
+ params["key"].each do |key|
238
268
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
239
269
  delete_or_add_queue job, params if job
240
270
  end
@@ -243,7 +273,8 @@ module Sidekiq
243
273
  end
244
274
 
245
275
  post "/scheduled/:key" do
246
- halt(404) unless key = route_params[:key]
276
+ key = route_params[:key]
277
+ halt(404) unless key
247
278
 
248
279
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
249
280
  delete_or_add_queue job, params if job
@@ -251,23 +282,23 @@ module Sidekiq
251
282
  redirect_with_query("#{root_path}scheduled")
252
283
  end
253
284
 
254
- get '/dashboard/stats' do
285
+ get "/dashboard/stats" do
255
286
  redirect "#{root_path}stats"
256
287
  end
257
288
 
258
- get '/stats' do
289
+ get "/stats" do
259
290
  sidekiq_stats = Sidekiq::Stats.new
260
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
291
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
261
292
  json(
262
293
  sidekiq: {
263
- processed: sidekiq_stats.processed,
264
- failed: sidekiq_stats.failed,
265
- busy: sidekiq_stats.workers_size,
266
- processes: sidekiq_stats.processes_size,
267
- enqueued: sidekiq_stats.enqueued,
268
- scheduled: sidekiq_stats.scheduled_size,
269
- retries: sidekiq_stats.retry_size,
270
- dead: sidekiq_stats.dead_size,
294
+ processed: sidekiq_stats.processed,
295
+ failed: sidekiq_stats.failed,
296
+ busy: sidekiq_stats.workers_size,
297
+ processes: sidekiq_stats.processes_size,
298
+ enqueued: sidekiq_stats.enqueued,
299
+ scheduled: sidekiq_stats.scheduled_size,
300
+ retries: sidekiq_stats.retry_size,
301
+ dead: sidekiq_stats.dead_size,
271
302
  default_latency: sidekiq_stats.default_queue_latency
272
303
  },
273
304
  redis: redis_stats,
@@ -275,60 +306,52 @@ module Sidekiq
275
306
  )
276
307
  end
277
308
 
278
- get '/stats/queues' do
309
+ get "/stats/queues" do
279
310
  json Sidekiq::Stats::Queues.new.lengths
280
311
  end
281
312
 
282
313
  def call(env)
283
314
  action = self.class.match(env)
284
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
315
+ return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
285
316
 
317
+ app = @klass
286
318
  resp = catch(:halt) do
287
- app = @klass
288
319
  self.class.run_befores(app, action)
289
- begin
290
- resp = action.instance_exec env, &action.block
291
- ensure
292
- self.class.run_afters(app, action)
293
- end
294
-
295
- resp
320
+ action.instance_exec env, &action.block
321
+ ensure
322
+ self.class.run_afters(app, action)
296
323
  end
297
324
 
298
- resp = case resp
325
+ case resp
299
326
  when Array
327
+ # redirects go here
300
328
  resp
301
329
  else
330
+ # rendered content goes here
302
331
  headers = {
303
- "Content-Type" => "text/html",
304
- "Cache-Control" => "no-cache",
305
- "Content-Language" => action.locale,
306
- "Content-Security-Policy" => CSP_HEADER
332
+ "content-type" => "text/html",
333
+ "cache-control" => "private, no-store",
334
+ "content-language" => action.locale,
335
+ "content-security-policy" => CSP_HEADER
307
336
  }
308
-
337
+ # we'll let Rack calculate Content-Length for us.
309
338
  [200, headers, [resp]]
310
339
  end
311
-
312
- resp[1] = resp[1].dup
313
-
314
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
315
-
316
- resp
317
340
  end
318
341
 
319
- def self.helpers(mod=nil, &block)
320
- if block_given?
342
+ def self.helpers(mod = nil, &block)
343
+ if block
321
344
  WebAction.class_eval(&block)
322
345
  else
323
346
  WebAction.send(:include, mod)
324
347
  end
325
348
  end
326
349
 
327
- def self.before(path=nil, &block)
350
+ def self.before(path = nil, &block)
328
351
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
329
352
  end
330
353
 
331
- def self.after(path=nil, &block)
354
+ def self.after(path = nil, &block)
332
355
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
333
356
  end
334
357
 
@@ -341,8 +364,8 @@ module Sidekiq
341
364
  end
342
365
 
343
366
  def self.run_hooks(hooks, app, action)
344
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
345
- each {|_,b| action.instance_exec(action.env, app, &b) }
367
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
368
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
346
369
  end
347
370
 
348
371
  def self.befores
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file originally based on authenticity_token.rb from the sinatra/rack-protection project
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2011-2017 Konstantin Haase
8
+ # Copyright (c) 2015-2017 Zachary Scott
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # 'Software'), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require "securerandom"
30
+ require "base64"
31
+ require "rack/request"
32
+
33
+ module Sidekiq
34
+ class Web
35
+ class CsrfProtection
36
+ def initialize(app, options = nil)
37
+ @app = app
38
+ end
39
+
40
+ def call(env)
41
+ accept?(env) ? admit(env) : deny(env)
42
+ end
43
+
44
+ private
45
+
46
+ def admit(env)
47
+ # On each successful request, we create a fresh masked token
48
+ # which will be used in any forms rendered for this request.
49
+ s = session(env)
50
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
51
+ env[:csrf_token] = mask_token(s[:csrf])
52
+ @app.call(env)
53
+ end
54
+
55
+ def safe?(env)
56
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
57
+ end
58
+
59
+ def logger(env)
60
+ @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
61
+ end
62
+
63
+ def deny(env)
64
+ logger(env).warn "attack prevented by #{self.class}"
65
+ [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
66
+ end
67
+
68
+ def session(env)
69
+ env["rack.session"] || fail(<<~EOM)
70
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
71
+ make sure you mount Sidekiq::Web *inside* your application routes:
72
+
73
+
74
+ Rails.application.routes.draw do
75
+ mount Sidekiq::Web => "/sidekiq"
76
+ ....
77
+ end
78
+
79
+
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
83
+
84
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
85
+
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
88
+
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run Sidekiq::Web
92
+
93
+ EOM
94
+ end
95
+
96
+ def accept?(env)
97
+ return true if safe?(env)
98
+
99
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
100
+ valid_token?(env, giventoken)
101
+ end
102
+
103
+ TOKEN_LENGTH = 32
104
+
105
+ # Checks that the token given to us as a parameter matches
106
+ # the token stored in the session.
107
+ def valid_token?(env, giventoken)
108
+ return false if giventoken.nil? || giventoken.empty?
109
+
110
+ begin
111
+ token = decode_token(giventoken)
112
+ rescue ArgumentError # client input is invalid
113
+ return false
114
+ end
115
+
116
+ sess = session(env)
117
+ localtoken = sess[:csrf]
118
+
119
+ # Checks that Rack::Session::Cookie actualy contains the csrf toekn
120
+ return false if localtoken.nil?
121
+
122
+ # Rotate the session token after every use
123
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
124
+
125
+ # See if it's actually a masked token or not. We should be able
126
+ # to handle any unmasked tokens that we've issued without error.
127
+
128
+ if unmasked_token?(token)
129
+ compare_with_real_token token, localtoken
130
+ elsif masked_token?(token)
131
+ unmasked = unmask_token(token)
132
+ compare_with_real_token unmasked, localtoken
133
+ else
134
+ false # Token is malformed
135
+ end
136
+ end
137
+
138
+ # Creates a masked version of the authenticity token that varies
139
+ # on each request. The masking is used to mitigate SSL attacks
140
+ # like BREACH.
141
+ def mask_token(token)
142
+ token = decode_token(token)
143
+ one_time_pad = SecureRandom.random_bytes(token.length)
144
+ encrypted_token = xor_byte_strings(one_time_pad, token)
145
+ masked_token = one_time_pad + encrypted_token
146
+ Base64.urlsafe_encode64(masked_token)
147
+ end
148
+
149
+ # Essentially the inverse of +mask_token+.
150
+ def unmask_token(masked_token)
151
+ # Split the token into the one-time pad and the encrypted
152
+ # value and decrypt it
153
+ token_length = masked_token.length / 2
154
+ one_time_pad = masked_token[0...token_length]
155
+ encrypted_token = masked_token[token_length..-1]
156
+ xor_byte_strings(one_time_pad, encrypted_token)
157
+ end
158
+
159
+ def unmasked_token?(token)
160
+ token.length == TOKEN_LENGTH
161
+ end
162
+
163
+ def masked_token?(token)
164
+ token.length == TOKEN_LENGTH * 2
165
+ end
166
+
167
+ def compare_with_real_token(token, local)
168
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
+ end
170
+
171
+ def decode_token(token)
172
+ Base64.urlsafe_decode64(token)
173
+ end
174
+
175
+ def xor_byte_strings(s1, s2)
176
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
177
+ end
178
+ end
179
+ end
180
+ end