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