sidekiq 4.2.10 → 7.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +932 -7
  3. data/LICENSE.txt +9 -0
  4. data/README.md +49 -50
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +218 -116
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  10. data/lib/generators/sidekiq/job_generator.rb +59 -0
  11. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  12. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  13. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  14. data/lib/sidekiq/api.rb +710 -322
  15. data/lib/sidekiq/capsule.rb +132 -0
  16. data/lib/sidekiq/cli.rb +268 -248
  17. data/lib/sidekiq/client.rb +153 -101
  18. data/lib/sidekiq/component.rb +90 -0
  19. data/lib/sidekiq/config.rb +311 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +63 -0
  22. data/lib/sidekiq/fetch.rb +50 -42
  23. data/lib/sidekiq/iterable_job.rb +55 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +294 -0
  29. data/lib/sidekiq/job.rb +385 -0
  30. data/lib/sidekiq/job_logger.rb +52 -0
  31. data/lib/sidekiq/job_retry.rb +305 -0
  32. data/lib/sidekiq/job_util.rb +109 -0
  33. data/lib/sidekiq/launcher.rb +208 -108
  34. data/lib/sidekiq/logger.rb +131 -0
  35. data/lib/sidekiq/manager.rb +43 -47
  36. data/lib/sidekiq/metrics/query.rb +158 -0
  37. data/lib/sidekiq/metrics/shared.rb +106 -0
  38. data/lib/sidekiq/metrics/tracking.rb +148 -0
  39. data/lib/sidekiq/middleware/chain.rb +113 -56
  40. data/lib/sidekiq/middleware/current_attributes.rb +128 -0
  41. data/lib/sidekiq/middleware/i18n.rb +9 -7
  42. data/lib/sidekiq/middleware/modules.rb +23 -0
  43. data/lib/sidekiq/monitor.rb +147 -0
  44. data/lib/sidekiq/paginator.rb +33 -15
  45. data/lib/sidekiq/processor.rb +188 -98
  46. data/lib/sidekiq/rails.rb +53 -92
  47. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  48. data/lib/sidekiq/redis_connection.rb +86 -77
  49. data/lib/sidekiq/ring_buffer.rb +32 -0
  50. data/lib/sidekiq/scheduled.rb +140 -51
  51. data/lib/sidekiq/sd_notify.rb +149 -0
  52. data/lib/sidekiq/systemd.rb +26 -0
  53. data/lib/sidekiq/testing/inline.rb +6 -5
  54. data/lib/sidekiq/testing.rb +95 -85
  55. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  56. data/lib/sidekiq/version.rb +7 -1
  57. data/lib/sidekiq/web/action.rb +40 -18
  58. data/lib/sidekiq/web/application.rb +189 -89
  59. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  60. data/lib/sidekiq/web/helpers.rb +239 -101
  61. data/lib/sidekiq/web/router.rb +28 -21
  62. data/lib/sidekiq/web.rb +123 -110
  63. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  64. data/lib/sidekiq.rb +97 -185
  65. data/sidekiq.gemspec +26 -27
  66. data/web/assets/images/apple-touch-icon.png +0 -0
  67. data/web/assets/javascripts/application.js +157 -61
  68. data/web/assets/javascripts/base-charts.js +106 -0
  69. data/web/assets/javascripts/chart.min.js +13 -0
  70. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  71. data/web/assets/javascripts/dashboard-charts.js +194 -0
  72. data/web/assets/javascripts/dashboard.js +43 -280
  73. data/web/assets/javascripts/metrics.js +298 -0
  74. data/web/assets/stylesheets/application-dark.css +147 -0
  75. data/web/assets/stylesheets/application-rtl.css +163 -0
  76. data/web/assets/stylesheets/application.css +176 -196
  77. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  78. data/web/assets/stylesheets/bootstrap.css +2 -2
  79. data/web/locales/ar.yml +87 -0
  80. data/web/locales/cs.yml +62 -62
  81. data/web/locales/da.yml +60 -53
  82. data/web/locales/de.yml +65 -53
  83. data/web/locales/el.yml +43 -24
  84. data/web/locales/en.yml +88 -64
  85. data/web/locales/es.yml +70 -53
  86. data/web/locales/fa.yml +65 -64
  87. data/web/locales/fr.yml +82 -62
  88. data/web/locales/gd.yml +98 -0
  89. data/web/locales/he.yml +80 -0
  90. data/web/locales/hi.yml +59 -59
  91. data/web/locales/it.yml +85 -54
  92. data/web/locales/ja.yml +74 -62
  93. data/web/locales/ko.yml +52 -52
  94. data/web/locales/lt.yml +83 -0
  95. data/web/locales/nb.yml +61 -61
  96. data/web/locales/nl.yml +52 -52
  97. data/web/locales/pl.yml +45 -45
  98. data/web/locales/pt-br.yml +82 -55
  99. data/web/locales/pt.yml +51 -51
  100. data/web/locales/ru.yml +68 -63
  101. data/web/locales/sv.yml +53 -53
  102. data/web/locales/ta.yml +60 -60
  103. data/web/locales/tr.yml +100 -0
  104. data/web/locales/uk.yml +85 -61
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +42 -16
  108. data/web/locales/zh-tw.yml +41 -8
  109. data/web/views/_footer.erb +20 -3
  110. data/web/views/_job_info.erb +21 -4
  111. data/web/views/_metrics_period_select.erb +12 -0
  112. data/web/views/_nav.erb +5 -19
  113. data/web/views/_paging.erb +3 -1
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +7 -7
  116. data/web/views/busy.erb +85 -31
  117. data/web/views/dashboard.erb +53 -20
  118. data/web/views/dead.erb +3 -3
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +17 -6
  121. data/web/views/metrics.erb +90 -0
  122. data/web/views/metrics_for_job.erb +59 -0
  123. data/web/views/morgue.erb +15 -16
  124. data/web/views/queue.erb +35 -25
  125. data/web/views/queues.erb +20 -4
  126. data/web/views/retries.erb +19 -16
  127. data/web/views/retry.erb +3 -3
  128. data/web/views/scheduled.erb +19 -17
  129. metadata +103 -194
  130. data/.github/contributing.md +0 -32
  131. data/.github/issue_template.md +0 -9
  132. data/.gitignore +0 -12
  133. data/.travis.yml +0 -18
  134. data/3.0-Upgrade.md +0 -70
  135. data/4.0-Upgrade.md +0 -53
  136. data/COMM-LICENSE +0 -95
  137. data/Ent-Changes.md +0 -173
  138. data/Gemfile +0 -29
  139. data/LICENSE +0 -9
  140. data/Pro-2.0-Upgrade.md +0 -138
  141. data/Pro-3.0-Upgrade.md +0 -44
  142. data/Pro-Changes.md +0 -628
  143. data/Rakefile +0 -12
  144. data/bin/sidekiqctl +0 -99
  145. data/code_of_conduct.md +0 -50
  146. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  147. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  148. data/lib/sidekiq/core_ext.rb +0 -119
  149. data/lib/sidekiq/exception_handler.rb +0 -31
  150. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  151. data/lib/sidekiq/extensions/active_record.rb +0 -40
  152. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  153. data/lib/sidekiq/extensions/generic_proxy.rb +0 -25
  154. data/lib/sidekiq/logging.rb +0 -106
  155. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  156. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  157. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  158. data/lib/sidekiq/util.rb +0 -63
  159. data/lib/sidekiq/worker.rb +0 -121
@@ -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_TEMPLATE = [
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' 'nonce-!placeholder!'",
19
+ "style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
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,67 @@ 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 and returns
53
+ # the size of the default queue
54
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
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.slice(*REDIS_KEYS)
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
+ x = params[:substr]
71
+ class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
72
+
73
+ q = Sidekiq::Metrics::Query.new
74
+ @period = h((params[:period] || "")[0..1])
75
+ @periods = METRICS_PERIODS
76
+ minutes = @periods.fetch(@period, @periods.values.first)
77
+ @query_result = q.top_jobs(minutes: minutes, class_filter: class_filter)
78
+
79
+ erb(:metrics)
80
+ end
81
+
82
+ get "/metrics/:name" do
83
+ @name = route_params[:name]
84
+ @period = h((params[:period] || "")[0..1])
85
+ q = Sidekiq::Metrics::Query.new
86
+ @periods = METRICS_PERIODS
87
+ minutes = @periods.fetch(@period, @periods.values.first)
88
+ @query_result = q.for_job(@name, minutes: minutes)
89
+ erb(:metrics_for_job)
90
+ end
91
+
40
92
  get "/busy" do
93
+ @count = (params["count"] || 100).to_i
94
+ (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
95
+
41
96
  erb(:busy)
42
97
  end
43
98
 
44
99
  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']
100
+ if params["identity"]
101
+ pro = Sidekiq::ProcessSet[params["identity"]]
102
+
103
+ pro.quiet! if params["quiet"]
104
+ pro.stop! if params["stop"]
49
105
  else
50
106
  processes.each do |pro|
51
- pro.quiet! if params['quiet']
52
- pro.stop! if params['stop']
107
+ next if pro.embedded?
108
+
109
+ pro.quiet! if params["quiet"]
110
+ pro.stop! if params["stop"]
53
111
  end
54
112
  end
55
113
 
@@ -62,42 +120,59 @@ module Sidekiq
62
120
  erb(:queues)
63
121
  end
64
122
 
123
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
124
+
65
125
  get "/queues/:name" do
66
126
  @name = route_params[:name]
67
127
 
68
- halt(404) unless @name
128
+ halt(404) if !@name || @name !~ QUEUE_NAME
69
129
 
70
- @count = (params['count'] || 25).to_i
130
+ @count = (params["count"] || 25).to_i
71
131
  @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) }
132
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
133
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
74
134
 
75
135
  erb(:queue)
76
136
  end
77
137
 
78
138
  post "/queues/:name" do
79
- Sidekiq::Queue.new(route_params[:name]).clear
139
+ queue = Sidekiq::Queue.new(route_params[:name])
140
+
141
+ if Sidekiq.pro? && params["pause"]
142
+ queue.pause!
143
+ elsif Sidekiq.pro? && params["unpause"]
144
+ queue.unpause!
145
+ else
146
+ queue.clear
147
+ end
80
148
 
81
149
  redirect "#{root_path}queues"
82
150
  end
83
151
 
84
152
  post "/queues/:name/delete" do
85
153
  name = route_params[:name]
86
- Sidekiq::Job.new(params['key_val'], name).delete
154
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
87
155
 
88
156
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
89
157
  end
90
158
 
91
- get '/morgue' do
92
- @count = (params['count'] || 25).to_i
93
- (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
94
- @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
159
+ get "/morgue" do
160
+ x = params[:substr]
161
+
162
+ if x && x != ""
163
+ @dead = search(Sidekiq::DeadSet.new, x)
164
+ else
165
+ @count = (params["count"] || 25).to_i
166
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
167
+ @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
168
+ end
95
169
 
96
170
  erb(:morgue)
97
171
  end
98
172
 
99
173
  get "/morgue/:key" do
100
- halt(404) unless key = route_params[:key]
174
+ key = route_params[:key]
175
+ halt(404) unless key
101
176
 
102
177
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
103
178
 
@@ -108,10 +183,10 @@ module Sidekiq
108
183
  end
109
184
  end
110
185
 
111
- post '/morgue' do
112
- redirect(request.path) unless params['key']
186
+ post "/morgue" do
187
+ redirect(request.path) unless url_params("key")
113
188
 
114
- params['key'].each do |key|
189
+ params["key"].each do |key|
115
190
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
116
191
  retry_or_delete_or_kill job, params if job
117
192
  end
@@ -132,7 +207,8 @@ module Sidekiq
132
207
  end
133
208
 
134
209
  post "/morgue/:key" do
135
- halt(404) unless key = route_params[:key]
210
+ key = route_params(:key)
211
+ halt(404) unless key
136
212
 
137
213
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
138
214
  retry_or_delete_or_kill job, params if job
@@ -140,10 +216,16 @@ module Sidekiq
140
216
  redirect_with_query("#{root_path}morgue")
141
217
  end
142
218
 
143
- get '/retries' do
144
- @count = (params['count'] || 25).to_i
145
- (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
146
- @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
219
+ get "/retries" do
220
+ x = url_params("substr")
221
+
222
+ if x && x != ""
223
+ @retries = search(Sidekiq::RetrySet.new, x)
224
+ else
225
+ @count = (params["count"] || 25).to_i
226
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
227
+ @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
228
+ end
147
229
 
148
230
  erb(:retries)
149
231
  end
@@ -158,10 +240,10 @@ module Sidekiq
158
240
  end
159
241
  end
160
242
 
161
- post '/retries' do
162
- redirect(request.path) unless params['key']
243
+ post "/retries" do
244
+ redirect(request.path) unless params["key"]
163
245
 
164
- params['key'].each do |key|
246
+ params["key"].each do |key|
165
247
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
166
248
  retry_or_delete_or_kill job, params if job
167
249
  end
@@ -181,6 +263,12 @@ module Sidekiq
181
263
  redirect "#{root_path}retries"
182
264
  end
183
265
 
266
+ post "/retries/all/kill" do
267
+ Sidekiq::RetrySet.new.kill_all
268
+
269
+ redirect "#{root_path}retries"
270
+ end
271
+
184
272
  post "/retries/:key" do
185
273
  job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
186
274
 
@@ -189,10 +277,16 @@ module Sidekiq
189
277
  redirect_with_query("#{root_path}retries")
190
278
  end
191
279
 
192
- get '/scheduled' do
193
- @count = (params['count'] || 25).to_i
194
- (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
195
- @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
280
+ get "/scheduled" do
281
+ x = params[:substr]
282
+
283
+ if x && x != ""
284
+ @scheduled = search(Sidekiq::ScheduledSet.new, x)
285
+ else
286
+ @count = (params["count"] || 25).to_i
287
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
288
+ @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
289
+ end
196
290
 
197
291
  erb(:scheduled)
198
292
  end
@@ -207,10 +301,10 @@ module Sidekiq
207
301
  end
208
302
  end
209
303
 
210
- post '/scheduled' do
211
- redirect(request.path) unless params['key']
304
+ post "/scheduled" do
305
+ redirect(request.path) unless params["key"]
212
306
 
213
- params['key'].each do |key|
307
+ params["key"].each do |key|
214
308
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
215
309
  delete_or_add_queue job, params if job
216
310
  end
@@ -219,7 +313,8 @@ module Sidekiq
219
313
  end
220
314
 
221
315
  post "/scheduled/:key" do
222
- halt(404) unless key = route_params[:key]
316
+ key = route_params[:key]
317
+ halt(404) unless key
223
318
 
224
319
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
225
320
  delete_or_add_queue job, params if job
@@ -227,88 +322,93 @@ module Sidekiq
227
322
  redirect_with_query("#{root_path}scheduled")
228
323
  end
229
324
 
230
- get '/dashboard/stats' do
325
+ get "/dashboard/stats" do
231
326
  redirect "#{root_path}stats"
232
327
  end
233
328
 
234
- get '/stats' do
329
+ get "/stats" do
235
330
  sidekiq_stats = Sidekiq::Stats.new
236
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
237
-
331
+ redis_stats = redis_info.slice(*REDIS_KEYS)
238
332
  json(
239
333
  sidekiq: {
240
- processed: sidekiq_stats.processed,
241
- failed: sidekiq_stats.failed,
242
- busy: sidekiq_stats.workers_size,
243
- processes: sidekiq_stats.processes_size,
244
- enqueued: sidekiq_stats.enqueued,
245
- scheduled: sidekiq_stats.scheduled_size,
246
- retries: sidekiq_stats.retry_size,
247
- dead: sidekiq_stats.dead_size,
334
+ processed: sidekiq_stats.processed,
335
+ failed: sidekiq_stats.failed,
336
+ busy: sidekiq_stats.workers_size,
337
+ processes: sidekiq_stats.processes_size,
338
+ enqueued: sidekiq_stats.enqueued,
339
+ scheduled: sidekiq_stats.scheduled_size,
340
+ retries: sidekiq_stats.retry_size,
341
+ dead: sidekiq_stats.dead_size,
248
342
  default_latency: sidekiq_stats.default_queue_latency
249
343
  },
250
- redis: redis_stats
344
+ redis: redis_stats,
345
+ server_utc_time: server_utc_time
251
346
  )
252
347
  end
253
348
 
254
- get '/stats/queues' do
255
- json Sidekiq::Stats::Queues.new.lengths
349
+ get "/stats/queues" do
350
+ json Sidekiq::Stats.new.queues
351
+ end
352
+
353
+ post "/change_locale" do
354
+ locale = params["locale"]
355
+
356
+ match = available_locales.find { |available|
357
+ locale == available
358
+ }
359
+
360
+ session[:locale] = match if match
361
+
362
+ reload_page
256
363
  end
257
364
 
258
365
  def call(env)
259
366
  action = self.class.match(env)
260
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
367
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
261
368
 
369
+ app = @klass
262
370
  resp = catch(:halt) do
263
- app = @klass
264
371
  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
372
+ action.instance_exec env, &action.block
373
+ ensure
374
+ self.class.run_afters(app, action)
272
375
  end
273
376
 
274
- resp = case resp
377
+ case resp
275
378
  when Array
379
+ # redirects go here
276
380
  resp
277
- when Integer
278
- [resp, {}, []]
279
381
  else
280
- type_header = case action.type
281
- when :json
282
- { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
283
- when String
284
- { "Content-Type" => (action.type || "text/html"), "Cache-Control" => "no-cache" }
285
- else
286
- { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
287
- end
288
-
289
- [200, type_header, [resp]]
382
+ # rendered content goes here
383
+ headers = {
384
+ Rack::CONTENT_TYPE => "text/html",
385
+ Rack::CACHE_CONTROL => "private, no-store",
386
+ Web::CONTENT_LANGUAGE => action.locale,
387
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
388
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
389
+ }
390
+ # we'll let Rack calculate Content-Length for us.
391
+ [200, headers, [resp]]
290
392
  end
393
+ end
291
394
 
292
- resp[1] = resp[1].dup
293
-
294
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
295
-
296
- resp
395
+ def process_csp(env, input)
396
+ input.gsub("!placeholder!", env[:csp_nonce])
297
397
  end
298
398
 
299
- def self.helpers(mod=nil, &block)
300
- if block_given?
399
+ def self.helpers(mod = nil, &block)
400
+ if block
301
401
  WebAction.class_eval(&block)
302
402
  else
303
403
  WebAction.send(:include, mod)
304
404
  end
305
405
  end
306
406
 
307
- def self.before(path=nil, &block)
407
+ def self.before(path = nil, &block)
308
408
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
309
409
  end
310
410
 
311
- def self.after(path=nil, &block)
411
+ def self.after(path = nil, &block)
312
412
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
313
413
  end
314
414
 
@@ -321,8 +421,8 @@ module Sidekiq
321
421
  end
322
422
 
323
423
  def self.run_hooks(hooks, app, action)
324
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
325
- each {|_,b| action.instance_exec(action.env, app, &b) }
424
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
425
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
326
426
  end
327
427
 
328
428
  def self.befores
@@ -0,0 +1,183 @@
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 "rack/request"
31
+
32
+ module Sidekiq
33
+ class Web
34
+ class CsrfProtection
35
+ def initialize(app, options = nil)
36
+ @app = app
37
+ end
38
+
39
+ def call(env)
40
+ accept?(env) ? admit(env) : deny(env)
41
+ end
42
+
43
+ private
44
+
45
+ def admit(env)
46
+ # On each successful request, we create a fresh masked token
47
+ # which will be used in any forms rendered for this request.
48
+ s = session(env)
49
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
50
+ env[:csrf_token] = mask_token(s[:csrf])
51
+ @app.call(env)
52
+ end
53
+
54
+ def safe?(env)
55
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
56
+ end
57
+
58
+ def logger(env)
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
60
+ end
61
+
62
+ def deny(env)
63
+ logger(env).warn "attack prevented by #{self.class}"
64
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
65
+ end
66
+
67
+ def session(env)
68
+ env["rack.session"] || fail(<<~EOM)
69
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
70
+ make sure you mount Sidekiq::Web *inside* your application routes:
71
+
72
+
73
+ Rails.application.routes.draw do
74
+ mount Sidekiq::Web => "/sidekiq"
75
+ ....
76
+ end
77
+
78
+
79
+ If this is a Rails app in API mode, you need to enable sessions.
80
+
81
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
82
+
83
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
84
+
85
+ # first, use IRB to create a shared secret key for sessions and commit it
86
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
87
+
88
+ # now use the secret with a session cookie middleware
89
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
90
+ run Sidekiq::Web
91
+
92
+ EOM
93
+ end
94
+
95
+ def accept?(env)
96
+ return true if safe?(env)
97
+
98
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
99
+ valid_token?(env, giventoken)
100
+ end
101
+
102
+ TOKEN_LENGTH = 32
103
+
104
+ # Checks that the token given to us as a parameter matches
105
+ # the token stored in the session.
106
+ def valid_token?(env, giventoken)
107
+ return false if giventoken.nil? || giventoken.empty?
108
+
109
+ begin
110
+ token = decode_token(giventoken)
111
+ rescue ArgumentError # client input is invalid
112
+ return false
113
+ end
114
+
115
+ sess = session(env)
116
+ localtoken = sess[:csrf]
117
+
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
119
+ return false if localtoken.nil?
120
+
121
+ # Rotate the session token after every use
122
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
123
+
124
+ # See if it's actually a masked token or not. We should be able
125
+ # to handle any unmasked tokens that we've issued without error.
126
+
127
+ if unmasked_token?(token)
128
+ compare_with_real_token token, localtoken
129
+ elsif masked_token?(token)
130
+ unmasked = unmask_token(token)
131
+ compare_with_real_token unmasked, localtoken
132
+ else
133
+ false # Token is malformed
134
+ end
135
+ end
136
+
137
+ # Creates a masked version of the authenticity token that varies
138
+ # on each request. The masking is used to mitigate SSL attacks
139
+ # like BREACH.
140
+ def mask_token(token)
141
+ token = decode_token(token)
142
+ one_time_pad = SecureRandom.random_bytes(token.length)
143
+ encrypted_token = xor_byte_strings(one_time_pad, token)
144
+ masked_token = one_time_pad + encrypted_token
145
+ encode_token(masked_token)
146
+ end
147
+
148
+ # Essentially the inverse of +mask_token+.
149
+ def unmask_token(masked_token)
150
+ # Split the token into the one-time pad and the encrypted
151
+ # value and decrypt it
152
+ token_length = masked_token.length / 2
153
+ one_time_pad = masked_token[0...token_length]
154
+ encrypted_token = masked_token[token_length..]
155
+ xor_byte_strings(one_time_pad, encrypted_token)
156
+ end
157
+
158
+ def unmasked_token?(token)
159
+ token.length == TOKEN_LENGTH
160
+ end
161
+
162
+ def masked_token?(token)
163
+ token.length == TOKEN_LENGTH * 2
164
+ end
165
+
166
+ def compare_with_real_token(token, local)
167
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
168
+ end
169
+
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
174
+ def decode_token(token)
175
+ token.tr("-_", "+/").unpack1("m0")
176
+ end
177
+
178
+ def xor_byte_strings(s1, s2)
179
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
180
+ end
181
+ end
182
+ end
183
+ end