sidekiq 5.1.1 → 7.1.2

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 (149) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +627 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -50
  5. data/bin/sidekiq +22 -3
  6. data/bin/sidekiqload +213 -115
  7. data/bin/sidekiqmon +11 -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 +566 -329
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +241 -256
  15. data/lib/sidekiq/client.rb +125 -102
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +278 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +49 -42
  21. data/lib/sidekiq/job.rb +374 -0
  22. data/lib/sidekiq/job_logger.rb +36 -9
  23. data/lib/sidekiq/job_retry.rb +147 -98
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +207 -103
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +43 -47
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +113 -56
  32. data/lib/sidekiq/middleware/current_attributes.rb +95 -0
  33. data/lib/sidekiq/middleware/i18n.rb +7 -7
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +146 -0
  36. data/lib/sidekiq/paginator.rb +28 -16
  37. data/lib/sidekiq/processor.rb +159 -107
  38. data/lib/sidekiq/rails.rb +54 -43
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +39 -81
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +139 -48
  43. data/lib/sidekiq/sd_notify.rb +149 -0
  44. data/lib/sidekiq/systemd.rb +24 -0
  45. data/lib/sidekiq/testing/inline.rb +6 -5
  46. data/lib/sidekiq/testing.rb +70 -88
  47. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  48. data/lib/sidekiq/version.rb +3 -1
  49. data/lib/sidekiq/web/action.rb +15 -11
  50. data/lib/sidekiq/web/application.rb +143 -77
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +144 -106
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +60 -111
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +94 -183
  57. data/sidekiq.gemspec +25 -23
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +130 -61
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +166 -0
  64. data/web/assets/javascripts/dashboard.js +36 -282
  65. data/web/assets/javascripts/metrics.js +264 -0
  66. data/web/assets/stylesheets/application-dark.css +147 -0
  67. data/web/assets/stylesheets/application-rtl.css +2 -95
  68. data/web/assets/stylesheets/application.css +134 -521
  69. data/web/assets/stylesheets/bootstrap.css +2 -2
  70. data/web/locales/ar.yml +71 -64
  71. data/web/locales/cs.yml +62 -62
  72. data/web/locales/da.yml +60 -53
  73. data/web/locales/de.yml +65 -53
  74. data/web/locales/el.yml +43 -24
  75. data/web/locales/en.yml +84 -65
  76. data/web/locales/es.yml +70 -54
  77. data/web/locales/fa.yml +65 -65
  78. data/web/locales/fr.yml +83 -62
  79. data/web/locales/gd.yml +99 -0
  80. data/web/locales/he.yml +65 -64
  81. data/web/locales/hi.yml +59 -59
  82. data/web/locales/it.yml +53 -53
  83. data/web/locales/ja.yml +75 -64
  84. data/web/locales/ko.yml +52 -52
  85. data/web/locales/lt.yml +83 -0
  86. data/web/locales/nb.yml +61 -61
  87. data/web/locales/nl.yml +52 -52
  88. data/web/locales/pl.yml +45 -45
  89. data/web/locales/pt-br.yml +63 -55
  90. data/web/locales/pt.yml +51 -51
  91. data/web/locales/ru.yml +68 -63
  92. data/web/locales/sv.yml +53 -53
  93. data/web/locales/ta.yml +60 -60
  94. data/web/locales/uk.yml +62 -61
  95. data/web/locales/ur.yml +64 -64
  96. data/web/locales/vi.yml +83 -0
  97. data/web/locales/zh-cn.yml +43 -16
  98. data/web/locales/zh-tw.yml +42 -8
  99. data/web/views/_footer.erb +8 -2
  100. data/web/views/_job_info.erb +21 -4
  101. data/web/views/_metrics_period_select.erb +12 -0
  102. data/web/views/_nav.erb +4 -18
  103. data/web/views/_paging.erb +2 -0
  104. data/web/views/_poll_link.erb +3 -6
  105. data/web/views/_summary.erb +7 -7
  106. data/web/views/busy.erb +75 -25
  107. data/web/views/dashboard.erb +58 -18
  108. data/web/views/dead.erb +3 -3
  109. data/web/views/layout.erb +4 -2
  110. data/web/views/metrics.erb +82 -0
  111. data/web/views/metrics_for_job.erb +68 -0
  112. data/web/views/morgue.erb +14 -15
  113. data/web/views/queue.erb +33 -23
  114. data/web/views/queues.erb +14 -4
  115. data/web/views/retries.erb +19 -16
  116. data/web/views/retry.erb +3 -3
  117. data/web/views/scheduled.erb +17 -15
  118. metadata +71 -140
  119. data/.github/contributing.md +0 -32
  120. data/.github/issue_template.md +0 -11
  121. data/.gitignore +0 -13
  122. data/.travis.yml +0 -14
  123. data/3.0-Upgrade.md +0 -70
  124. data/4.0-Upgrade.md +0 -53
  125. data/5.0-Upgrade.md +0 -56
  126. data/COMM-LICENSE +0 -95
  127. data/Ent-Changes.md +0 -210
  128. data/Gemfile +0 -8
  129. data/LICENSE +0 -9
  130. data/Pro-2.0-Upgrade.md +0 -138
  131. data/Pro-3.0-Upgrade.md +0 -44
  132. data/Pro-4.0-Upgrade.md +0 -35
  133. data/Pro-Changes.md +0 -716
  134. data/Rakefile +0 -8
  135. data/bin/sidekiqctl +0 -99
  136. data/code_of_conduct.md +0 -50
  137. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  138. data/lib/sidekiq/core_ext.rb +0 -1
  139. data/lib/sidekiq/delay.rb +0 -41
  140. data/lib/sidekiq/exception_handler.rb +0 -29
  141. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  142. data/lib/sidekiq/extensions/active_record.rb +0 -40
  143. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  144. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  145. data/lib/sidekiq/logging.rb +0 -122
  146. data/lib/sidekiq/middleware/server/active_record.rb +0 -22
  147. data/lib/sidekiq/middleware/server/active_record_cache.rb +0 -11
  148. data/lib/sidekiq/util.rb +0 -66
  149. data/lib/sidekiq/worker.rb +0 -204
@@ -4,9 +4,28 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length".freeze
8
- CONTENT_TYPE = "Content-Type".freeze
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]
8
+ CSP_HEADER = [
9
+ "default-src 'self' https: http:",
10
+ "child-src 'self'",
11
+ "connect-src 'self' https: http: wss: ws:",
12
+ "font-src 'self' https: http:",
13
+ "frame-src 'self'",
14
+ "img-src 'self' https: http: data:",
15
+ "manifest-src 'self'",
16
+ "media-src 'self'",
17
+ "object-src 'none'",
18
+ "script-src 'self' https: http: 'unsafe-inline'",
19
+ "style-src 'self' https: http: 'unsafe-inline'",
20
+ "worker-src 'self'",
21
+ "base-uri 'self'"
22
+ ].join("; ").freeze
23
+ METRICS_PERIODS = {
24
+ "1h" => 60,
25
+ "2h" => 120,
26
+ "4h" => 240,
27
+ "8h" => 480
28
+ }
10
29
 
11
30
  def initialize(klass)
12
31
  @klass = klass
@@ -28,28 +47,63 @@ module Sidekiq
28
47
  # nothing, backwards compatibility
29
48
  end
30
49
 
50
+ head "/" do
51
+ # HEAD / is the cheapest heartbeat possible,
52
+ # it hits Redis to ensure connectivity
53
+ Sidekiq.redis { |c| c.llen("queue:default") }
54
+ ""
55
+ end
56
+
31
57
  get "/" do
32
- @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
33
- stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
58
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
59
+ days = (params["days"] || 30).to_i
60
+ return halt(401) if days < 1 || days > 180
61
+
62
+ stats_history = Sidekiq::Stats::History.new(days)
34
63
  @processed_history = stats_history.processed
35
64
  @failed_history = stats_history.failed
36
65
 
37
66
  erb(:dashboard)
38
67
  end
39
68
 
69
+ get "/metrics" do
70
+ q = Sidekiq::Metrics::Query.new
71
+ @period = h((params[:period] || "")[0..1])
72
+ @periods = METRICS_PERIODS
73
+ minutes = @periods.fetch(@period, @periods.values.first)
74
+ @query_result = q.top_jobs(minutes: minutes)
75
+ erb(:metrics)
76
+ end
77
+
78
+ get "/metrics/:name" do
79
+ @name = route_params[:name]
80
+ @period = h((params[:period] || "")[0..1])
81
+ q = Sidekiq::Metrics::Query.new
82
+ @periods = METRICS_PERIODS
83
+ minutes = @periods.fetch(@period, @periods.values.first)
84
+ @query_result = q.for_job(@name, minutes: minutes)
85
+ erb(:metrics_for_job)
86
+ end
87
+
40
88
  get "/busy" do
89
+ @count = (params["count"] || 100).to_i
90
+ (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
91
+
41
92
  erb(:busy)
42
93
  end
43
94
 
44
95
  post "/busy" do
45
- if params['identity']
46
- p = Sidekiq::Process.new('identity' => params['identity'])
47
- p.quiet! if params['quiet']
48
- p.stop! if params['stop']
96
+ if params["identity"]
97
+ pro = Sidekiq::ProcessSet[params["identity"]]
98
+
99
+ pro.quiet! if params["quiet"]
100
+ pro.stop! if params["stop"]
49
101
  else
50
102
  processes.each do |pro|
51
- pro.quiet! if params['quiet']
52
- pro.stop! if params['stop']
103
+ next if pro.embedded?
104
+
105
+ pro.quiet! if params["quiet"]
106
+ pro.stop! if params["stop"]
53
107
  end
54
108
  end
55
109
 
@@ -62,42 +116,53 @@ module Sidekiq
62
116
  erb(:queues)
63
117
  end
64
118
 
119
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
120
+
65
121
  get "/queues/:name" do
66
122
  @name = route_params[:name]
67
123
 
68
- halt(404) unless @name
124
+ halt(404) if !@name || @name !~ QUEUE_NAME
69
125
 
70
- @count = (params['count'] || 25).to_i
126
+ @count = (params["count"] || 25).to_i
71
127
  @queue = Sidekiq::Queue.new(@name)
72
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
73
- @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
128
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
129
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
74
130
 
75
131
  erb(:queue)
76
132
  end
77
133
 
78
134
  post "/queues/:name" do
79
- Sidekiq::Queue.new(route_params[:name]).clear
135
+ queue = Sidekiq::Queue.new(route_params[:name])
136
+
137
+ if Sidekiq.pro? && params["pause"]
138
+ queue.pause!
139
+ elsif Sidekiq.pro? && params["unpause"]
140
+ queue.unpause!
141
+ else
142
+ queue.clear
143
+ end
80
144
 
81
145
  redirect "#{root_path}queues"
82
146
  end
83
147
 
84
148
  post "/queues/:name/delete" do
85
149
  name = route_params[:name]
86
- Sidekiq::Job.new(params['key_val'], name).delete
150
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
87
151
 
88
152
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
89
153
  end
90
154
 
91
- get '/morgue' do
92
- @count = (params['count'] || 25).to_i
93
- (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
155
+ get "/morgue" do
156
+ @count = (params["count"] || 25).to_i
157
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
94
158
  @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
95
159
 
96
160
  erb(:morgue)
97
161
  end
98
162
 
99
163
  get "/morgue/:key" do
100
- halt(404) unless key = route_params[:key]
164
+ key = route_params[:key]
165
+ halt(404) unless key
101
166
 
102
167
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
103
168
 
@@ -108,10 +173,10 @@ module Sidekiq
108
173
  end
109
174
  end
110
175
 
111
- post '/morgue' do
112
- redirect(request.path) unless params['key']
176
+ post "/morgue" do
177
+ redirect(request.path) unless params["key"]
113
178
 
114
- params['key'].each do |key|
179
+ params["key"].each do |key|
115
180
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
116
181
  retry_or_delete_or_kill job, params if job
117
182
  end
@@ -132,7 +197,8 @@ module Sidekiq
132
197
  end
133
198
 
134
199
  post "/morgue/:key" do
135
- halt(404) unless key = route_params[:key]
200
+ key = route_params[:key]
201
+ halt(404) unless key
136
202
 
137
203
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
138
204
  retry_or_delete_or_kill job, params if job
@@ -140,9 +206,9 @@ module Sidekiq
140
206
  redirect_with_query("#{root_path}morgue")
141
207
  end
142
208
 
143
- get '/retries' do
144
- @count = (params['count'] || 25).to_i
145
- (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
209
+ get "/retries" do
210
+ @count = (params["count"] || 25).to_i
211
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
146
212
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
147
213
 
148
214
  erb(:retries)
@@ -158,10 +224,10 @@ module Sidekiq
158
224
  end
159
225
  end
160
226
 
161
- post '/retries' do
162
- redirect(request.path) unless params['key']
227
+ post "/retries" do
228
+ redirect(request.path) unless params["key"]
163
229
 
164
- params['key'].each do |key|
230
+ params["key"].each do |key|
165
231
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
166
232
  retry_or_delete_or_kill job, params if job
167
233
  end
@@ -181,6 +247,12 @@ module Sidekiq
181
247
  redirect "#{root_path}retries"
182
248
  end
183
249
 
250
+ post "/retries/all/kill" do
251
+ Sidekiq::RetrySet.new.kill_all
252
+
253
+ redirect "#{root_path}retries"
254
+ end
255
+
184
256
  post "/retries/:key" do
185
257
  job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
186
258
 
@@ -189,9 +261,9 @@ module Sidekiq
189
261
  redirect_with_query("#{root_path}retries")
190
262
  end
191
263
 
192
- get '/scheduled' do
193
- @count = (params['count'] || 25).to_i
194
- (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
264
+ get "/scheduled" do
265
+ @count = (params["count"] || 25).to_i
266
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
195
267
  @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
196
268
 
197
269
  erb(:scheduled)
@@ -207,10 +279,10 @@ module Sidekiq
207
279
  end
208
280
  end
209
281
 
210
- post '/scheduled' do
211
- redirect(request.path) unless params['key']
282
+ post "/scheduled" do
283
+ redirect(request.path) unless params["key"]
212
284
 
213
- params['key'].each do |key|
285
+ params["key"].each do |key|
214
286
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
215
287
  delete_or_add_queue job, params if job
216
288
  end
@@ -219,7 +291,8 @@ module Sidekiq
219
291
  end
220
292
 
221
293
  post "/scheduled/:key" do
222
- halt(404) unless key = route_params[:key]
294
+ key = route_params[:key]
295
+ halt(404) unless key
223
296
 
224
297
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
225
298
  delete_or_add_queue job, params if job
@@ -227,23 +300,23 @@ module Sidekiq
227
300
  redirect_with_query("#{root_path}scheduled")
228
301
  end
229
302
 
230
- get '/dashboard/stats' do
303
+ get "/dashboard/stats" do
231
304
  redirect "#{root_path}stats"
232
305
  end
233
306
 
234
- get '/stats' do
307
+ get "/stats" do
235
308
  sidekiq_stats = Sidekiq::Stats.new
236
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
309
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
237
310
  json(
238
311
  sidekiq: {
239
- processed: sidekiq_stats.processed,
240
- failed: sidekiq_stats.failed,
241
- busy: sidekiq_stats.workers_size,
242
- processes: sidekiq_stats.processes_size,
243
- enqueued: sidekiq_stats.enqueued,
244
- scheduled: sidekiq_stats.scheduled_size,
245
- retries: sidekiq_stats.retry_size,
246
- dead: sidekiq_stats.dead_size,
312
+ processed: sidekiq_stats.processed,
313
+ failed: sidekiq_stats.failed,
314
+ busy: sidekiq_stats.workers_size,
315
+ processes: sidekiq_stats.processes_size,
316
+ enqueued: sidekiq_stats.enqueued,
317
+ scheduled: sidekiq_stats.scheduled_size,
318
+ retries: sidekiq_stats.retry_size,
319
+ dead: sidekiq_stats.dead_size,
247
320
  default_latency: sidekiq_stats.default_queue_latency
248
321
  },
249
322
  redis: redis_stats,
@@ -251,59 +324,52 @@ module Sidekiq
251
324
  )
252
325
  end
253
326
 
254
- get '/stats/queues' do
255
- json Sidekiq::Stats::Queues.new.lengths
327
+ get "/stats/queues" do
328
+ json Sidekiq::Stats.new.queues
256
329
  end
257
330
 
258
331
  def call(env)
259
332
  action = self.class.match(env)
260
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
333
+ return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
261
334
 
335
+ app = @klass
262
336
  resp = catch(:halt) do
263
- app = @klass
264
337
  self.class.run_befores(app, action)
265
- begin
266
- resp = action.instance_exec env, &action.block
267
- ensure
268
- self.class.run_afters(app, action)
269
- end
270
-
271
- resp
338
+ action.instance_exec env, &action.block
339
+ ensure
340
+ self.class.run_afters(app, action)
272
341
  end
273
342
 
274
- resp = case resp
343
+ case resp
275
344
  when Array
345
+ # redirects go here
276
346
  resp
277
347
  else
348
+ # rendered content goes here
278
349
  headers = {
279
- "Content-Type" => "text/html",
280
- "Cache-Control" => "no-cache",
281
- "Content-Language" => action.locale,
350
+ "content-type" => "text/html",
351
+ "cache-control" => "private, no-store",
352
+ "content-language" => action.locale,
353
+ "content-security-policy" => CSP_HEADER
282
354
  }
283
-
355
+ # we'll let Rack calculate Content-Length for us.
284
356
  [200, headers, [resp]]
285
357
  end
286
-
287
- resp[1] = resp[1].dup
288
-
289
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
290
-
291
- resp
292
358
  end
293
359
 
294
- def self.helpers(mod=nil, &block)
295
- if block_given?
360
+ def self.helpers(mod = nil, &block)
361
+ if block
296
362
  WebAction.class_eval(&block)
297
363
  else
298
364
  WebAction.send(:include, mod)
299
365
  end
300
366
  end
301
367
 
302
- def self.before(path=nil, &block)
368
+ def self.before(path = nil, &block)
303
369
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
304
370
  end
305
371
 
306
- def self.after(path=nil, &block)
372
+ def self.after(path = nil, &block)
307
373
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
308
374
  end
309
375
 
@@ -316,8 +382,8 @@ module Sidekiq
316
382
  end
317
383
 
318
384
  def self.run_hooks(hooks, app, action)
319
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
320
- each {|_,b| action.instance_exec(action.env, app, &b) }
385
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
386
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
321
387
  end
322
388
 
323
389
  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..]
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