sidekiq 5.1.3 → 7.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +756 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +48 -51
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +213 -115
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/generators/sidekiq/job_generator.rb +59 -0
  10. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  11. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  12. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  13. data/lib/sidekiq/api.rb +640 -330
  14. data/lib/sidekiq/capsule.rb +132 -0
  15. data/lib/sidekiq/cli.rb +244 -257
  16. data/lib/sidekiq/client.rb +132 -103
  17. data/lib/sidekiq/component.rb +68 -0
  18. data/lib/sidekiq/config.rb +293 -0
  19. data/lib/sidekiq/deploy.rb +64 -0
  20. data/lib/sidekiq/embedded.rb +63 -0
  21. data/lib/sidekiq/fetch.rb +49 -42
  22. data/lib/sidekiq/iterable_job.rb +55 -0
  23. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  24. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  25. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  26. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  27. data/lib/sidekiq/job/iterable.rb +231 -0
  28. data/lib/sidekiq/job.rb +385 -0
  29. data/lib/sidekiq/job_logger.rb +49 -12
  30. data/lib/sidekiq/job_retry.rb +167 -103
  31. data/lib/sidekiq/job_util.rb +109 -0
  32. data/lib/sidekiq/launcher.rb +209 -102
  33. data/lib/sidekiq/logger.rb +131 -0
  34. data/lib/sidekiq/manager.rb +43 -46
  35. data/lib/sidekiq/metrics/query.rb +158 -0
  36. data/lib/sidekiq/metrics/shared.rb +97 -0
  37. data/lib/sidekiq/metrics/tracking.rb +148 -0
  38. data/lib/sidekiq/middleware/chain.rb +113 -56
  39. data/lib/sidekiq/middleware/current_attributes.rb +113 -0
  40. data/lib/sidekiq/middleware/i18n.rb +7 -7
  41. data/lib/sidekiq/middleware/modules.rb +23 -0
  42. data/lib/sidekiq/monitor.rb +147 -0
  43. data/lib/sidekiq/paginator.rb +28 -16
  44. data/lib/sidekiq/processor.rb +175 -112
  45. data/lib/sidekiq/rails.rb +54 -39
  46. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  47. data/lib/sidekiq/redis_connection.rb +65 -86
  48. data/lib/sidekiq/ring_buffer.rb +31 -0
  49. data/lib/sidekiq/scheduled.rb +139 -48
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +26 -0
  52. data/lib/sidekiq/testing/inline.rb +6 -5
  53. data/lib/sidekiq/testing.rb +95 -94
  54. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  55. data/lib/sidekiq/version.rb +3 -1
  56. data/lib/sidekiq/web/action.rb +22 -12
  57. data/lib/sidekiq/web/application.rb +225 -76
  58. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  59. data/lib/sidekiq/web/helpers.rb +215 -118
  60. data/lib/sidekiq/web/router.rb +23 -19
  61. data/lib/sidekiq/web.rb +114 -106
  62. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  63. data/lib/sidekiq.rb +95 -182
  64. data/sidekiq.gemspec +26 -23
  65. data/web/assets/images/apple-touch-icon.png +0 -0
  66. data/web/assets/javascripts/application.js +157 -61
  67. data/web/assets/javascripts/base-charts.js +106 -0
  68. data/web/assets/javascripts/chart.min.js +13 -0
  69. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  70. data/web/assets/javascripts/dashboard-charts.js +192 -0
  71. data/web/assets/javascripts/dashboard.js +35 -283
  72. data/web/assets/javascripts/metrics.js +298 -0
  73. data/web/assets/stylesheets/application-dark.css +147 -0
  74. data/web/assets/stylesheets/application-rtl.css +10 -93
  75. data/web/assets/stylesheets/application.css +169 -522
  76. data/web/assets/stylesheets/bootstrap.css +2 -2
  77. data/web/locales/ar.yml +71 -64
  78. data/web/locales/cs.yml +62 -62
  79. data/web/locales/da.yml +60 -53
  80. data/web/locales/de.yml +65 -53
  81. data/web/locales/el.yml +43 -24
  82. data/web/locales/en.yml +86 -65
  83. data/web/locales/es.yml +70 -54
  84. data/web/locales/fa.yml +65 -65
  85. data/web/locales/fr.yml +83 -62
  86. data/web/locales/gd.yml +99 -0
  87. data/web/locales/he.yml +65 -64
  88. data/web/locales/hi.yml +59 -59
  89. data/web/locales/it.yml +53 -53
  90. data/web/locales/ja.yml +75 -64
  91. data/web/locales/ko.yml +52 -52
  92. data/web/locales/lt.yml +83 -0
  93. data/web/locales/nb.yml +61 -61
  94. data/web/locales/nl.yml +52 -52
  95. data/web/locales/pl.yml +45 -45
  96. data/web/locales/pt-br.yml +83 -55
  97. data/web/locales/pt.yml +51 -51
  98. data/web/locales/ru.yml +68 -63
  99. data/web/locales/sv.yml +53 -53
  100. data/web/locales/ta.yml +60 -60
  101. data/web/locales/tr.yml +101 -0
  102. data/web/locales/uk.yml +62 -61
  103. data/web/locales/ur.yml +64 -64
  104. data/web/locales/vi.yml +83 -0
  105. data/web/locales/zh-cn.yml +43 -16
  106. data/web/locales/zh-tw.yml +42 -8
  107. data/web/views/_footer.erb +18 -3
  108. data/web/views/_job_info.erb +21 -4
  109. data/web/views/_metrics_period_select.erb +12 -0
  110. data/web/views/_nav.erb +4 -18
  111. data/web/views/_paging.erb +2 -0
  112. data/web/views/_poll_link.erb +3 -6
  113. data/web/views/_summary.erb +7 -7
  114. data/web/views/busy.erb +79 -29
  115. data/web/views/dashboard.erb +49 -19
  116. data/web/views/dead.erb +3 -3
  117. data/web/views/filtering.erb +7 -0
  118. data/web/views/layout.erb +9 -7
  119. data/web/views/metrics.erb +91 -0
  120. data/web/views/metrics_for_job.erb +59 -0
  121. data/web/views/morgue.erb +14 -15
  122. data/web/views/queue.erb +33 -23
  123. data/web/views/queues.erb +19 -5
  124. data/web/views/retries.erb +19 -16
  125. data/web/views/retry.erb +3 -3
  126. data/web/views/scheduled.erb +17 -15
  127. metadata +84 -129
  128. data/.github/contributing.md +0 -32
  129. data/.github/issue_template.md +0 -11
  130. data/.gitignore +0 -13
  131. data/.travis.yml +0 -14
  132. data/3.0-Upgrade.md +0 -70
  133. data/4.0-Upgrade.md +0 -53
  134. data/5.0-Upgrade.md +0 -56
  135. data/COMM-LICENSE +0 -95
  136. data/Ent-Changes.md +0 -216
  137. data/Gemfile +0 -8
  138. data/LICENSE +0 -9
  139. data/Pro-2.0-Upgrade.md +0 -138
  140. data/Pro-3.0-Upgrade.md +0 -44
  141. data/Pro-4.0-Upgrade.md +0 -35
  142. data/Pro-Changes.md +0 -729
  143. data/Rakefile +0 -8
  144. data/bin/sidekiqctl +0 -99
  145. data/code_of_conduct.md +0 -50
  146. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  147. data/lib/sidekiq/core_ext.rb +0 -1
  148. data/lib/sidekiq/delay.rb +0 -42
  149. data/lib/sidekiq/exception_handler.rb +0 -29
  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 -31
  154. data/lib/sidekiq/logging.rb +0 -122
  155. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  156. data/lib/sidekiq/util.rb +0 -66
  157. 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"
8
- CONTENT_TYPE = "Content-Type"
9
- REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
7
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
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,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 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.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,135 @@ 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
329
+ end
330
+
331
+ ########
332
+ # Filtering
333
+
334
+ get "/filter/metrics" do
335
+ redirect "#{root_path}metrics"
336
+ end
337
+
338
+ post "/filter/metrics" do
339
+ x = params[:substr]
340
+ q = Sidekiq::Metrics::Query.new
341
+ @period = h((params[:period] || "")[0..1])
342
+ @periods = METRICS_PERIODS
343
+ minutes = @periods.fetch(@period, @periods.values.first)
344
+ @query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
345
+
346
+ erb :metrics
347
+ end
348
+
349
+ get "/filter/retries" do
350
+ x = params[:substr]
351
+ return redirect "#{root_path}retries" unless x && x != ""
352
+
353
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
354
+ erb :retries
355
+ end
356
+
357
+ post "/filter/retries" do
358
+ x = params[:substr]
359
+ return redirect "#{root_path}retries" unless x && x != ""
360
+
361
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
362
+ erb :retries
363
+ end
364
+
365
+ get "/filter/scheduled" do
366
+ x = params[:substr]
367
+ return redirect "#{root_path}scheduled" unless x && x != ""
368
+
369
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
370
+ erb :scheduled
371
+ end
372
+
373
+ post "/filter/scheduled" do
374
+ x = params[:substr]
375
+ return redirect "#{root_path}scheduled" unless x && x != ""
376
+
377
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
378
+ erb :scheduled
379
+ end
380
+
381
+ get "/filter/dead" do
382
+ x = params[:substr]
383
+ return redirect "#{root_path}morgue" unless x && x != ""
384
+
385
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
386
+ erb :morgue
387
+ end
388
+
389
+ post "/filter/dead" do
390
+ x = params[:substr]
391
+ return redirect "#{root_path}morgue" unless x && x != ""
392
+
393
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
394
+ erb :morgue
395
+ end
396
+
397
+ post "/change_locale" do
398
+ locale = params["locale"]
399
+
400
+ match = available_locales.find { |available|
401
+ locale == available
402
+ }
403
+
404
+ session[:locale] = match if match
405
+
406
+ reload_page
256
407
  end
257
408
 
258
409
  def call(env)
259
410
  action = self.class.match(env)
260
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
411
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
261
412
 
413
+ app = @klass
262
414
  resp = catch(:halt) do
263
- app = @klass
264
415
  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
416
+ action.instance_exec env, &action.block
417
+ ensure
418
+ self.class.run_afters(app, action)
272
419
  end
273
420
 
274
- resp = case resp
421
+ case resp
275
422
  when Array
423
+ # redirects go here
276
424
  resp
277
425
  else
426
+ # rendered content goes here
278
427
  headers = {
279
- "Content-Type" => "text/html",
280
- "Cache-Control" => "no-cache",
281
- "Content-Language" => action.locale,
428
+ Rack::CONTENT_TYPE => "text/html",
429
+ Rack::CACHE_CONTROL => "private, no-store",
430
+ Web::CONTENT_LANGUAGE => action.locale,
431
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
432
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
282
433
  }
283
-
434
+ # we'll let Rack calculate Content-Length for us.
284
435
  [200, headers, [resp]]
285
436
  end
437
+ end
286
438
 
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
439
+ def process_csp(env, input)
440
+ input.gsub("!placeholder!", env[:csp_nonce])
292
441
  end
293
442
 
294
- def self.helpers(mod=nil, &block)
295
- if block_given?
443
+ def self.helpers(mod = nil, &block)
444
+ if block
296
445
  WebAction.class_eval(&block)
297
446
  else
298
447
  WebAction.send(:include, mod)
299
448
  end
300
449
  end
301
450
 
302
- def self.before(path=nil, &block)
451
+ def self.before(path = nil, &block)
303
452
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
304
453
  end
305
454
 
306
- def self.after(path=nil, &block)
455
+ def self.after(path = nil, &block)
307
456
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
308
457
  end
309
458
 
@@ -316,8 +465,8 @@ module Sidekiq
316
465
  end
317
466
 
318
467
  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) }
468
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
469
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
321
470
  end
322
471
 
323
472
  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