sidekiq 4.2.4 → 6.2.1

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 (134) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +445 -0
  3. data/LICENSE +1 -1
  4. data/README.md +21 -34
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +28 -38
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  9. data/lib/generators/sidekiq/templates/worker_test.rb.erb +2 -2
  10. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  11. data/lib/sidekiq/api.rb +347 -213
  12. data/lib/sidekiq/cli.rb +221 -212
  13. data/lib/sidekiq/client.rb +75 -52
  14. data/lib/sidekiq/delay.rb +41 -0
  15. data/lib/sidekiq/exception_handler.rb +12 -16
  16. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  17. data/lib/sidekiq/extensions/active_record.rb +13 -10
  18. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  19. data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
  20. data/lib/sidekiq/fetch.rb +38 -31
  21. data/lib/sidekiq/job_logger.rb +63 -0
  22. data/lib/sidekiq/job_retry.rb +263 -0
  23. data/lib/sidekiq/launcher.rb +169 -70
  24. data/lib/sidekiq/logger.rb +166 -0
  25. data/lib/sidekiq/manager.rb +17 -20
  26. data/lib/sidekiq/middleware/chain.rb +15 -5
  27. data/lib/sidekiq/middleware/i18n.rb +5 -7
  28. data/lib/sidekiq/monitor.rb +133 -0
  29. data/lib/sidekiq/paginator.rb +18 -14
  30. data/lib/sidekiq/processor.rb +161 -70
  31. data/lib/sidekiq/rails.rb +30 -73
  32. data/lib/sidekiq/redis_connection.rb +67 -20
  33. data/lib/sidekiq/scheduled.rb +61 -35
  34. data/lib/sidekiq/sd_notify.rb +149 -0
  35. data/lib/sidekiq/systemd.rb +24 -0
  36. data/lib/sidekiq/testing/inline.rb +2 -1
  37. data/lib/sidekiq/testing.rb +54 -26
  38. data/lib/sidekiq/util.rb +48 -15
  39. data/lib/sidekiq/version.rb +2 -1
  40. data/lib/sidekiq/web/action.rb +15 -15
  41. data/lib/sidekiq/web/application.rb +112 -89
  42. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  43. data/lib/sidekiq/web/helpers.rb +153 -73
  44. data/lib/sidekiq/web/router.rb +27 -19
  45. data/lib/sidekiq/web.rb +64 -109
  46. data/lib/sidekiq/worker.rb +164 -41
  47. data/lib/sidekiq.rb +86 -60
  48. data/sidekiq.gemspec +24 -22
  49. data/web/assets/images/apple-touch-icon.png +0 -0
  50. data/web/assets/javascripts/application.js +25 -27
  51. data/web/assets/javascripts/dashboard.js +34 -38
  52. data/web/assets/stylesheets/application-dark.css +160 -0
  53. data/web/assets/stylesheets/application-rtl.css +246 -0
  54. data/web/assets/stylesheets/application.css +402 -12
  55. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  56. data/web/assets/stylesheets/bootstrap.css +2 -2
  57. data/web/locales/ar.yml +81 -0
  58. data/web/locales/de.yml +14 -2
  59. data/web/locales/en.yml +4 -0
  60. data/web/locales/es.yml +4 -3
  61. data/web/locales/fa.yml +80 -0
  62. data/web/locales/fr.yml +3 -3
  63. data/web/locales/he.yml +79 -0
  64. data/web/locales/ja.yml +9 -4
  65. data/web/locales/lt.yml +83 -0
  66. data/web/locales/pl.yml +4 -4
  67. data/web/locales/ru.yml +4 -0
  68. data/web/locales/ur.yml +80 -0
  69. data/web/locales/vi.yml +83 -0
  70. data/web/views/_footer.erb +5 -2
  71. data/web/views/_job_info.erb +3 -2
  72. data/web/views/_nav.erb +4 -18
  73. data/web/views/_paging.erb +1 -1
  74. data/web/views/busy.erb +57 -19
  75. data/web/views/dashboard.erb +3 -3
  76. data/web/views/dead.erb +2 -2
  77. data/web/views/layout.erb +13 -2
  78. data/web/views/morgue.erb +19 -12
  79. data/web/views/queue.erb +22 -12
  80. data/web/views/queues.erb +13 -3
  81. data/web/views/retries.erb +22 -13
  82. data/web/views/retry.erb +3 -3
  83. data/web/views/scheduled.erb +7 -4
  84. metadata +42 -194
  85. data/.github/contributing.md +0 -32
  86. data/.github/issue_template.md +0 -4
  87. data/.gitignore +0 -12
  88. data/.travis.yml +0 -12
  89. data/3.0-Upgrade.md +0 -70
  90. data/4.0-Upgrade.md +0 -53
  91. data/COMM-LICENSE +0 -95
  92. data/Ent-Changes.md +0 -146
  93. data/Gemfile +0 -29
  94. data/Pro-2.0-Upgrade.md +0 -138
  95. data/Pro-3.0-Upgrade.md +0 -44
  96. data/Pro-Changes.md +0 -585
  97. data/Rakefile +0 -9
  98. data/bin/sidekiqctl +0 -99
  99. data/code_of_conduct.md +0 -50
  100. data/lib/sidekiq/core_ext.rb +0 -106
  101. data/lib/sidekiq/logging.rb +0 -106
  102. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  103. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  104. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  105. data/test/config.yml +0 -9
  106. data/test/env_based_config.yml +0 -11
  107. data/test/fake_env.rb +0 -1
  108. data/test/fixtures/en.yml +0 -2
  109. data/test/helper.rb +0 -75
  110. data/test/test_actors.rb +0 -138
  111. data/test/test_api.rb +0 -528
  112. data/test/test_cli.rb +0 -418
  113. data/test/test_client.rb +0 -266
  114. data/test/test_exception_handler.rb +0 -56
  115. data/test/test_extensions.rb +0 -127
  116. data/test/test_fetch.rb +0 -50
  117. data/test/test_launcher.rb +0 -95
  118. data/test/test_logging.rb +0 -35
  119. data/test/test_manager.rb +0 -50
  120. data/test/test_middleware.rb +0 -158
  121. data/test/test_processor.rb +0 -235
  122. data/test/test_rails.rb +0 -22
  123. data/test/test_redis_connection.rb +0 -132
  124. data/test/test_retry.rb +0 -326
  125. data/test/test_retry_exhausted.rb +0 -149
  126. data/test/test_scheduled.rb +0 -115
  127. data/test/test_scheduling.rb +0 -58
  128. data/test/test_sidekiq.rb +0 -107
  129. data/test/test_testing.rb +0 -143
  130. data/test/test_testing_fake.rb +0 -357
  131. data/test/test_testing_inline.rb +0 -94
  132. data/test/test_util.rb +0 -13
  133. data/test/test_web.rb +0 -726
  134. data/test/test_web_helpers.rb +0 -54
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sidekiq
4
4
  class WebAction
5
- RACK_SESSION = 'rack.session'.freeze
5
+ RACK_SESSION = "rack.session"
6
6
 
7
7
  attr_accessor :env, :block, :type
8
8
 
@@ -15,18 +15,18 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, res
18
+ throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
19
19
  end
20
20
 
21
21
  def redirect(location)
22
- throw :halt, [302, { "Location" => "#{request.base_url}#{location}" }, []]
22
+ throw :halt, [302, {"Location" => "#{request.base_url}#{location}"}, []]
23
23
  end
24
24
 
25
25
  def params
26
- indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
26
+ indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
27
27
 
28
28
  indifferent_hash.merge! request.params
29
- route_params.each {|k,v| indifferent_hash[k.to_s] = v }
29
+ route_params.each { |k, v| indifferent_hash[k.to_s] = v }
30
30
 
31
31
  indifferent_hash
32
32
  end
@@ -39,15 +39,15 @@ module Sidekiq
39
39
  env[RACK_SESSION]
40
40
  end
41
41
 
42
- def content_type(type)
43
- @type = type
44
- end
45
-
46
42
  def erb(content, options = {})
47
- if content.kind_of? Symbol
43
+ if content.is_a? Symbol
48
44
  unless respond_to?(:"_erb_#{content}")
49
45
  src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
50
- WebAction.class_eval("def _erb_#{content}\n#{src}\n end")
46
+ WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
+ def _erb_#{content}
48
+ #{src}
49
+ end
50
+ RUBY
51
51
  end
52
52
  end
53
53
 
@@ -68,22 +68,22 @@ module Sidekiq
68
68
  end
69
69
 
70
70
  def json(payload)
71
- [200, { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }, [Sidekiq.dump_json(payload)]]
71
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "no-cache"}, [Sidekiq.dump_json(payload)]]
72
72
  end
73
73
 
74
74
  def initialize(env, block)
75
75
  @_erb = false
76
76
  @env = env
77
77
  @block = block
78
- @@files ||= {}
78
+ @files ||= {}
79
79
  end
80
80
 
81
81
  private
82
82
 
83
83
  def _erb(file, locals)
84
- locals.each {|k, v| define_singleton_method(k){ v } } if locals
84
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
85
85
 
86
- if file.kind_of?(String)
86
+ if file.is_a?(String)
87
87
  ERB.new(file).result(binding)
88
88
  else
89
89
  send(:"_erb_#{file}")
@@ -4,9 +4,22 @@ 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
10
23
 
11
24
  def initialize(klass)
12
25
  @klass = klass
@@ -28,13 +41,16 @@ module Sidekiq
28
41
  # nothing, backwards compatibility
29
42
  end
30
43
 
31
- get "" do
32
- redirect(root_path)
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
+ ""
33
49
  end
34
50
 
35
51
  get "/" do
36
- @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
37
- stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
52
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
53
+ stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)
38
54
  @processed_history = stats_history.processed
39
55
  @failed_history = stats_history.failed
40
56
 
@@ -46,14 +62,14 @@ module Sidekiq
46
62
  end
47
63
 
48
64
  post "/busy" do
49
- if params['identity']
50
- p = Sidekiq::Process.new('identity' => params['identity'])
51
- p.quiet! if params['quiet']
52
- p.stop! if params['stop']
65
+ if params["identity"]
66
+ p = Sidekiq::Process.new("identity" => params["identity"])
67
+ p.quiet! if params["quiet"]
68
+ p.stop! if params["stop"]
53
69
  else
54
70
  processes.each do |pro|
55
- pro.quiet! if params['quiet']
56
- pro.stop! if params['stop']
71
+ pro.quiet! if params["quiet"]
72
+ pro.stop! if params["stop"]
57
73
  end
58
74
  end
59
75
 
@@ -66,42 +82,53 @@ module Sidekiq
66
82
  erb(:queues)
67
83
  end
68
84
 
85
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
86
+
69
87
  get "/queues/:name" do
70
88
  @name = route_params[:name]
71
89
 
72
- halt(404) unless @name
90
+ halt(404) if !@name || @name !~ QUEUE_NAME
73
91
 
74
- @count = (params['count'] || 25).to_i
92
+ @count = (params["count"] || 25).to_i
75
93
  @queue = Sidekiq::Queue.new(@name)
76
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
94
+ (@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
77
95
  @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
78
96
 
79
97
  erb(:queue)
80
98
  end
81
99
 
82
100
  post "/queues/:name" do
83
- Sidekiq::Queue.new(route_params[:name]).clear
101
+ queue = Sidekiq::Queue.new(route_params[:name])
102
+
103
+ if Sidekiq.pro? && params["pause"]
104
+ queue.pause!
105
+ elsif Sidekiq.pro? && params["unpause"]
106
+ queue.unpause!
107
+ else
108
+ queue.clear
109
+ end
84
110
 
85
111
  redirect "#{root_path}queues"
86
112
  end
87
113
 
88
114
  post "/queues/:name/delete" do
89
115
  name = route_params[:name]
90
- Sidekiq::Job.new(params['key_val'], name).delete
116
+ Sidekiq::Job.new(params["key_val"], name).delete
91
117
 
92
- redirect_with_query("#{root_path}queues/#{name}")
118
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
93
119
  end
94
120
 
95
- get '/morgue' do
96
- @count = (params['count'] || 25).to_i
97
- (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
121
+ get "/morgue" do
122
+ @count = (params["count"] || 25).to_i
123
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
98
124
  @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
99
125
 
100
126
  erb(:morgue)
101
127
  end
102
128
 
103
129
  get "/morgue/:key" do
104
- halt(404) unless key = route_params[:key]
130
+ key = route_params[:key]
131
+ halt(404) unless key
105
132
 
106
133
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
107
134
 
@@ -112,10 +139,10 @@ module Sidekiq
112
139
  end
113
140
  end
114
141
 
115
- post '/morgue' do
116
- redirect(request.path) unless params['key']
142
+ post "/morgue" do
143
+ redirect(request.path) unless params["key"]
117
144
 
118
- params['key'].each do |key|
145
+ params["key"].each do |key|
119
146
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
120
147
  retry_or_delete_or_kill job, params if job
121
148
  end
@@ -136,7 +163,8 @@ module Sidekiq
136
163
  end
137
164
 
138
165
  post "/morgue/:key" do
139
- halt(404) unless key = route_params[:key]
166
+ key = route_params[:key]
167
+ halt(404) unless key
140
168
 
141
169
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
142
170
  retry_or_delete_or_kill job, params if job
@@ -144,9 +172,9 @@ module Sidekiq
144
172
  redirect_with_query("#{root_path}morgue")
145
173
  end
146
174
 
147
- get '/retries' do
148
- @count = (params['count'] || 25).to_i
149
- (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
175
+ get "/retries" do
176
+ @count = (params["count"] || 25).to_i
177
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
150
178
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
151
179
 
152
180
  erb(:retries)
@@ -162,10 +190,10 @@ module Sidekiq
162
190
  end
163
191
  end
164
192
 
165
- post '/retries' do
166
- redirect(request.path) unless params['key']
193
+ post "/retries" do
194
+ redirect(request.path) unless params["key"]
167
195
 
168
- params['key'].each do |key|
196
+ params["key"].each do |key|
169
197
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
170
198
  retry_or_delete_or_kill job, params if job
171
199
  end
@@ -185,6 +213,12 @@ module Sidekiq
185
213
  redirect "#{root_path}retries"
186
214
  end
187
215
 
216
+ post "/retries/all/kill" do
217
+ Sidekiq::RetrySet.new.kill_all
218
+
219
+ redirect "#{root_path}retries"
220
+ end
221
+
188
222
  post "/retries/:key" do
189
223
  job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
190
224
 
@@ -193,9 +227,9 @@ module Sidekiq
193
227
  redirect_with_query("#{root_path}retries")
194
228
  end
195
229
 
196
- get '/scheduled' do
197
- @count = (params['count'] || 25).to_i
198
- (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
230
+ get "/scheduled" do
231
+ @count = (params["count"] || 25).to_i
232
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
199
233
  @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
200
234
 
201
235
  erb(:scheduled)
@@ -211,10 +245,10 @@ module Sidekiq
211
245
  end
212
246
  end
213
247
 
214
- post '/scheduled' do
215
- redirect(request.path) unless params['key']
248
+ post "/scheduled" do
249
+ redirect(request.path) unless params["key"]
216
250
 
217
- params['key'].each do |key|
251
+ params["key"].each do |key|
218
252
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
219
253
  delete_or_add_queue job, params if job
220
254
  end
@@ -223,7 +257,8 @@ module Sidekiq
223
257
  end
224
258
 
225
259
  post "/scheduled/:key" do
226
- halt(404) unless key = route_params[:key]
260
+ key = route_params[:key]
261
+ halt(404) unless key
227
262
 
228
263
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
229
264
  delete_or_add_queue job, params if job
@@ -231,88 +266,76 @@ module Sidekiq
231
266
  redirect_with_query("#{root_path}scheduled")
232
267
  end
233
268
 
234
- get '/dashboard/stats' do
269
+ get "/dashboard/stats" do
235
270
  redirect "#{root_path}stats"
236
271
  end
237
272
 
238
- get '/stats' do
273
+ get "/stats" do
239
274
  sidekiq_stats = Sidekiq::Stats.new
240
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
241
-
275
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
242
276
  json(
243
277
  sidekiq: {
244
- processed: sidekiq_stats.processed,
245
- failed: sidekiq_stats.failed,
246
- busy: sidekiq_stats.workers_size,
247
- processes: sidekiq_stats.processes_size,
248
- enqueued: sidekiq_stats.enqueued,
249
- scheduled: sidekiq_stats.scheduled_size,
250
- retries: sidekiq_stats.retry_size,
251
- dead: sidekiq_stats.dead_size,
278
+ processed: sidekiq_stats.processed,
279
+ failed: sidekiq_stats.failed,
280
+ busy: sidekiq_stats.workers_size,
281
+ processes: sidekiq_stats.processes_size,
282
+ enqueued: sidekiq_stats.enqueued,
283
+ scheduled: sidekiq_stats.scheduled_size,
284
+ retries: sidekiq_stats.retry_size,
285
+ dead: sidekiq_stats.dead_size,
252
286
  default_latency: sidekiq_stats.default_queue_latency
253
287
  },
254
- redis: redis_stats
288
+ redis: redis_stats,
289
+ server_utc_time: server_utc_time
255
290
  )
256
291
  end
257
292
 
258
- get '/stats/queues' do
293
+ get "/stats/queues" do
259
294
  json Sidekiq::Stats::Queues.new.lengths
260
295
  end
261
296
 
262
297
  def call(env)
263
298
  action = self.class.match(env)
264
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
299
+ return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action
265
300
 
266
- resp = catch(:halt) do
267
- app = @klass
301
+ app = @klass
302
+ resp = catch(:halt) do # rubocop:disable Standard/SemanticBlocks
268
303
  self.class.run_befores(app, action)
269
- begin
270
- resp = action.instance_exec env, &action.block
271
- ensure
272
- self.class.run_afters(app, action)
273
- end
274
-
275
- resp
304
+ action.instance_exec env, &action.block
305
+ ensure
306
+ self.class.run_afters(app, action)
276
307
  end
277
308
 
278
- resp = case resp
309
+ case resp
279
310
  when Array
311
+ # redirects go here
280
312
  resp
281
- when Fixnum
282
- [resp, {}, []]
283
313
  else
284
- type_header = case action.type
285
- when :json
286
- { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
287
- when String
288
- { "Content-Type" => action.type, "Cache-Control" => "no-cache" }
289
- else
290
- { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
291
- end
292
-
293
- [200, type_header, [resp]]
314
+ # rendered content goes here
315
+ headers = {
316
+ "Content-Type" => "text/html",
317
+ "Cache-Control" => "no-cache",
318
+ "Content-Language" => action.locale,
319
+ "Content-Security-Policy" => CSP_HEADER
320
+ }
321
+ # we'll let Rack calculate Content-Length for us.
322
+ [200, headers, [resp]]
294
323
  end
295
-
296
- resp[1] = resp[1].dup
297
-
298
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
299
-
300
- resp
301
324
  end
302
325
 
303
- def self.helpers(mod=nil, &block)
304
- if block_given?
326
+ def self.helpers(mod = nil, &block)
327
+ if block
305
328
  WebAction.class_eval(&block)
306
329
  else
307
330
  WebAction.send(:include, mod)
308
331
  end
309
332
  end
310
333
 
311
- def self.before(path=nil, &block)
334
+ def self.before(path = nil, &block)
312
335
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
313
336
  end
314
337
 
315
- def self.after(path=nil, &block)
338
+ def self.after(path = nil, &block)
316
339
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
317
340
  end
318
341
 
@@ -325,8 +348,8 @@ module Sidekiq
325
348
  end
326
349
 
327
350
  def self.run_hooks(hooks, app, action)
328
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
329
- each {|_,b| action.instance_exec(action.env, app, &b) }
351
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
352
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
330
353
  end
331
354
 
332
355
  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.strict_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.strict_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