sidekiq 5.2.4 → 7.2.4
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.
- checksums.yaml +4 -4
- data/Changes.md +672 -8
- data/LICENSE.txt +9 -0
- data/README.md +48 -51
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +22 -3
- data/bin/sidekiqload +213 -115
- data/bin/sidekiqmon +11 -0
- data/lib/generators/sidekiq/job_generator.rb +57 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +623 -352
- data/lib/sidekiq/capsule.rb +127 -0
- data/lib/sidekiq/cli.rb +214 -229
- data/lib/sidekiq/client.rb +127 -102
- data/lib/sidekiq/component.rb +68 -0
- data/lib/sidekiq/config.rb +287 -0
- data/lib/sidekiq/deploy.rb +62 -0
- data/lib/sidekiq/embedded.rb +61 -0
- data/lib/sidekiq/fetch.rb +49 -42
- data/lib/sidekiq/job.rb +374 -0
- data/lib/sidekiq/job_logger.rb +33 -7
- data/lib/sidekiq/job_retry.rb +157 -108
- data/lib/sidekiq/job_util.rb +107 -0
- data/lib/sidekiq/launcher.rb +206 -106
- data/lib/sidekiq/logger.rb +131 -0
- data/lib/sidekiq/manager.rb +43 -46
- data/lib/sidekiq/metrics/query.rb +156 -0
- data/lib/sidekiq/metrics/shared.rb +95 -0
- data/lib/sidekiq/metrics/tracking.rb +140 -0
- data/lib/sidekiq/middleware/chain.rb +113 -56
- data/lib/sidekiq/middleware/current_attributes.rb +95 -0
- data/lib/sidekiq/middleware/i18n.rb +7 -7
- data/lib/sidekiq/middleware/modules.rb +21 -0
- data/lib/sidekiq/monitor.rb +146 -0
- data/lib/sidekiq/paginator.rb +28 -16
- data/lib/sidekiq/processor.rb +126 -117
- data/lib/sidekiq/rails.rb +52 -38
- data/lib/sidekiq/redis_client_adapter.rb +111 -0
- data/lib/sidekiq/redis_connection.rb +41 -112
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +112 -50
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +24 -0
- data/lib/sidekiq/testing/inline.rb +6 -5
- data/lib/sidekiq/testing.rb +91 -90
- data/lib/sidekiq/transaction_aware_client.rb +51 -0
- data/lib/sidekiq/version.rb +3 -1
- data/lib/sidekiq/web/action.rb +20 -11
- data/lib/sidekiq/web/application.rb +202 -80
- data/lib/sidekiq/web/csrf_protection.rb +183 -0
- data/lib/sidekiq/web/helpers.rb +165 -114
- data/lib/sidekiq/web/router.rb +23 -19
- data/lib/sidekiq/web.rb +68 -107
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +92 -182
- data/sidekiq.gemspec +25 -16
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +152 -61
- data/web/assets/javascripts/base-charts.js +106 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +182 -0
- data/web/assets/javascripts/dashboard.js +35 -293
- data/web/assets/javascripts/metrics.js +298 -0
- data/web/assets/stylesheets/application-dark.css +147 -0
- data/web/assets/stylesheets/application-rtl.css +10 -93
- data/web/assets/stylesheets/application.css +124 -522
- data/web/assets/stylesheets/bootstrap.css +1 -1
- data/web/locales/ar.yml +71 -65
- data/web/locales/cs.yml +62 -62
- data/web/locales/da.yml +60 -53
- data/web/locales/de.yml +65 -53
- data/web/locales/el.yml +43 -24
- data/web/locales/en.yml +86 -66
- data/web/locales/es.yml +70 -54
- data/web/locales/fa.yml +65 -65
- data/web/locales/fr.yml +83 -62
- data/web/locales/gd.yml +99 -0
- data/web/locales/he.yml +65 -64
- data/web/locales/hi.yml +59 -59
- data/web/locales/it.yml +53 -53
- data/web/locales/ja.yml +75 -64
- data/web/locales/ko.yml +52 -52
- data/web/locales/lt.yml +83 -0
- data/web/locales/nb.yml +61 -61
- data/web/locales/nl.yml +52 -52
- data/web/locales/pl.yml +45 -45
- data/web/locales/pt-br.yml +83 -55
- data/web/locales/pt.yml +51 -51
- data/web/locales/ru.yml +68 -63
- data/web/locales/sv.yml +53 -53
- data/web/locales/ta.yml +60 -60
- data/web/locales/uk.yml +62 -61
- data/web/locales/ur.yml +64 -64
- data/web/locales/vi.yml +83 -0
- data/web/locales/zh-cn.yml +43 -16
- data/web/locales/zh-tw.yml +42 -8
- data/web/views/_footer.erb +18 -3
- data/web/views/_job_info.erb +21 -4
- data/web/views/_metrics_period_select.erb +12 -0
- data/web/views/_nav.erb +1 -1
- data/web/views/_paging.erb +2 -0
- data/web/views/_poll_link.erb +3 -6
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +79 -29
- data/web/views/dashboard.erb +48 -18
- data/web/views/dead.erb +3 -3
- data/web/views/filtering.erb +7 -0
- data/web/views/layout.erb +3 -1
- data/web/views/metrics.erb +91 -0
- data/web/views/metrics_for_job.erb +59 -0
- data/web/views/morgue.erb +14 -15
- data/web/views/queue.erb +33 -24
- data/web/views/queues.erb +19 -5
- data/web/views/retries.erb +16 -17
- data/web/views/retry.erb +3 -3
- data/web/views/scheduled.erb +17 -15
- metadata +71 -72
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -11
- data/.gitignore +0 -15
- data/.travis.yml +0 -17
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/5.0-Upgrade.md +0 -56
- data/Appraisals +0 -9
- data/COMM-LICENSE +0 -95
- data/Ent-Changes.md +0 -225
- data/Gemfile +0 -29
- data/LICENSE +0 -9
- data/Pro-2.0-Upgrade.md +0 -138
- data/Pro-3.0-Upgrade.md +0 -44
- data/Pro-4.0-Upgrade.md +0 -35
- data/Pro-Changes.md +0 -752
- data/Rakefile +0 -9
- data/bin/sidekiqctl +0 -237
- data/code_of_conduct.md +0 -50
- data/gemfiles/rails_4.gemfile +0 -31
- data/gemfiles/rails_5.gemfile +0 -31
- data/lib/generators/sidekiq/worker_generator.rb +0 -49
- data/lib/sidekiq/core_ext.rb +0 -1
- data/lib/sidekiq/delay.rb +0 -42
- data/lib/sidekiq/exception_handler.rb +0 -29
- data/lib/sidekiq/extensions/action_mailer.rb +0 -57
- data/lib/sidekiq/extensions/active_record.rb +0 -40
- data/lib/sidekiq/extensions/class_methods.rb +0 -40
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
- data/lib/sidekiq/logging.rb +0 -122
- data/lib/sidekiq/middleware/server/active_record.rb +0 -23
- data/lib/sidekiq/util.rb +0 -66
- data/lib/sidekiq/worker.rb +0 -215
|
@@ -4,9 +4,7 @@ module Sidekiq
|
|
|
4
4
|
class WebApplication
|
|
5
5
|
extend WebRouter
|
|
6
6
|
|
|
7
|
-
|
|
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]
|
|
10
8
|
CSP_HEADER = [
|
|
11
9
|
"default-src 'self' https: http:",
|
|
12
10
|
"child-src 'self'",
|
|
@@ -17,11 +15,17 @@ module Sidekiq
|
|
|
17
15
|
"manifest-src 'self'",
|
|
18
16
|
"media-src 'self'",
|
|
19
17
|
"object-src 'none'",
|
|
20
|
-
"script-src 'self' https: http:
|
|
18
|
+
"script-src 'self' https: http:",
|
|
21
19
|
"style-src 'self' https: http: 'unsafe-inline'",
|
|
22
20
|
"worker-src 'self'",
|
|
23
21
|
"base-uri 'self'"
|
|
24
|
-
].join(
|
|
22
|
+
].join("; ").freeze
|
|
23
|
+
METRICS_PERIODS = {
|
|
24
|
+
"1h" => 60,
|
|
25
|
+
"2h" => 120,
|
|
26
|
+
"4h" => 240,
|
|
27
|
+
"8h" => 480
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
def initialize(klass)
|
|
27
31
|
@klass = klass
|
|
@@ -43,28 +47,63 @@ module Sidekiq
|
|
|
43
47
|
# nothing, backwards compatibility
|
|
44
48
|
end
|
|
45
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
|
+
|
|
46
57
|
get "/" do
|
|
47
|
-
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
|
48
|
-
|
|
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)
|
|
49
63
|
@processed_history = stats_history.processed
|
|
50
64
|
@failed_history = stats_history.failed
|
|
51
65
|
|
|
52
66
|
erb(:dashboard)
|
|
53
67
|
end
|
|
54
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
|
+
|
|
55
88
|
get "/busy" do
|
|
89
|
+
@count = (params["count"] || 100).to_i
|
|
90
|
+
(@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
|
|
91
|
+
|
|
56
92
|
erb(:busy)
|
|
57
93
|
end
|
|
58
94
|
|
|
59
95
|
post "/busy" do
|
|
60
|
-
if params[
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
96
|
+
if params["identity"]
|
|
97
|
+
pro = Sidekiq::ProcessSet[params["identity"]]
|
|
98
|
+
|
|
99
|
+
pro.quiet! if params["quiet"]
|
|
100
|
+
pro.stop! if params["stop"]
|
|
64
101
|
else
|
|
65
102
|
processes.each do |pro|
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
next if pro.embedded?
|
|
104
|
+
|
|
105
|
+
pro.quiet! if params["quiet"]
|
|
106
|
+
pro.stop! if params["stop"]
|
|
68
107
|
end
|
|
69
108
|
end
|
|
70
109
|
|
|
@@ -77,42 +116,53 @@ module Sidekiq
|
|
|
77
116
|
erb(:queues)
|
|
78
117
|
end
|
|
79
118
|
|
|
119
|
+
QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
|
|
120
|
+
|
|
80
121
|
get "/queues/:name" do
|
|
81
122
|
@name = route_params[:name]
|
|
82
123
|
|
|
83
|
-
halt(404)
|
|
124
|
+
halt(404) if !@name || @name !~ QUEUE_NAME
|
|
84
125
|
|
|
85
|
-
@count = (params[
|
|
126
|
+
@count = (params["count"] || 25).to_i
|
|
86
127
|
@queue = Sidekiq::Queue.new(@name)
|
|
87
|
-
(@current_page, @total_size, @
|
|
88
|
-
@
|
|
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) }
|
|
89
130
|
|
|
90
131
|
erb(:queue)
|
|
91
132
|
end
|
|
92
133
|
|
|
93
134
|
post "/queues/:name" do
|
|
94
|
-
Sidekiq::Queue.new(route_params[:name])
|
|
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
|
|
95
144
|
|
|
96
145
|
redirect "#{root_path}queues"
|
|
97
146
|
end
|
|
98
147
|
|
|
99
148
|
post "/queues/:name/delete" do
|
|
100
149
|
name = route_params[:name]
|
|
101
|
-
Sidekiq::
|
|
150
|
+
Sidekiq::JobRecord.new(params["key_val"], name).delete
|
|
102
151
|
|
|
103
152
|
redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
|
|
104
153
|
end
|
|
105
154
|
|
|
106
|
-
get
|
|
107
|
-
@count = (params[
|
|
108
|
-
(@current_page, @total_size, @dead) = page("dead", params[
|
|
155
|
+
get "/morgue" do
|
|
156
|
+
@count = (params["count"] || 25).to_i
|
|
157
|
+
(@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
|
|
109
158
|
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
|
110
159
|
|
|
111
160
|
erb(:morgue)
|
|
112
161
|
end
|
|
113
162
|
|
|
114
163
|
get "/morgue/:key" do
|
|
115
|
-
|
|
164
|
+
key = route_params[:key]
|
|
165
|
+
halt(404) unless key
|
|
116
166
|
|
|
117
167
|
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
|
118
168
|
|
|
@@ -123,10 +173,10 @@ module Sidekiq
|
|
|
123
173
|
end
|
|
124
174
|
end
|
|
125
175
|
|
|
126
|
-
post
|
|
127
|
-
redirect(request.path) unless params[
|
|
176
|
+
post "/morgue" do
|
|
177
|
+
redirect(request.path) unless params["key"]
|
|
128
178
|
|
|
129
|
-
params[
|
|
179
|
+
params["key"].each do |key|
|
|
130
180
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
|
131
181
|
retry_or_delete_or_kill job, params if job
|
|
132
182
|
end
|
|
@@ -147,7 +197,8 @@ module Sidekiq
|
|
|
147
197
|
end
|
|
148
198
|
|
|
149
199
|
post "/morgue/:key" do
|
|
150
|
-
|
|
200
|
+
key = route_params[:key]
|
|
201
|
+
halt(404) unless key
|
|
151
202
|
|
|
152
203
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
|
153
204
|
retry_or_delete_or_kill job, params if job
|
|
@@ -155,9 +206,9 @@ module Sidekiq
|
|
|
155
206
|
redirect_with_query("#{root_path}morgue")
|
|
156
207
|
end
|
|
157
208
|
|
|
158
|
-
get
|
|
159
|
-
@count = (params[
|
|
160
|
-
(@current_page, @total_size, @retries) = page("retry", params[
|
|
209
|
+
get "/retries" do
|
|
210
|
+
@count = (params["count"] || 25).to_i
|
|
211
|
+
(@current_page, @total_size, @retries) = page("retry", params["page"], @count)
|
|
161
212
|
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
|
162
213
|
|
|
163
214
|
erb(:retries)
|
|
@@ -173,10 +224,10 @@ module Sidekiq
|
|
|
173
224
|
end
|
|
174
225
|
end
|
|
175
226
|
|
|
176
|
-
post
|
|
177
|
-
redirect(request.path) unless params[
|
|
227
|
+
post "/retries" do
|
|
228
|
+
redirect(request.path) unless params["key"]
|
|
178
229
|
|
|
179
|
-
params[
|
|
230
|
+
params["key"].each do |key|
|
|
180
231
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
|
181
232
|
retry_or_delete_or_kill job, params if job
|
|
182
233
|
end
|
|
@@ -210,9 +261,9 @@ module Sidekiq
|
|
|
210
261
|
redirect_with_query("#{root_path}retries")
|
|
211
262
|
end
|
|
212
263
|
|
|
213
|
-
get
|
|
214
|
-
@count = (params[
|
|
215
|
-
(@current_page, @total_size, @scheduled) = page("schedule", params[
|
|
264
|
+
get "/scheduled" do
|
|
265
|
+
@count = (params["count"] || 25).to_i
|
|
266
|
+
(@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
|
|
216
267
|
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
|
217
268
|
|
|
218
269
|
erb(:scheduled)
|
|
@@ -228,10 +279,10 @@ module Sidekiq
|
|
|
228
279
|
end
|
|
229
280
|
end
|
|
230
281
|
|
|
231
|
-
post
|
|
232
|
-
redirect(request.path) unless params[
|
|
282
|
+
post "/scheduled" do
|
|
283
|
+
redirect(request.path) unless params["key"]
|
|
233
284
|
|
|
234
|
-
params[
|
|
285
|
+
params["key"].each do |key|
|
|
235
286
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
|
236
287
|
delete_or_add_queue job, params if job
|
|
237
288
|
end
|
|
@@ -240,7 +291,8 @@ module Sidekiq
|
|
|
240
291
|
end
|
|
241
292
|
|
|
242
293
|
post "/scheduled/:key" do
|
|
243
|
-
|
|
294
|
+
key = route_params[:key]
|
|
295
|
+
halt(404) unless key
|
|
244
296
|
|
|
245
297
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
|
246
298
|
delete_or_add_queue job, params if job
|
|
@@ -248,23 +300,23 @@ module Sidekiq
|
|
|
248
300
|
redirect_with_query("#{root_path}scheduled")
|
|
249
301
|
end
|
|
250
302
|
|
|
251
|
-
get
|
|
303
|
+
get "/dashboard/stats" do
|
|
252
304
|
redirect "#{root_path}stats"
|
|
253
305
|
end
|
|
254
306
|
|
|
255
|
-
get
|
|
307
|
+
get "/stats" do
|
|
256
308
|
sidekiq_stats = Sidekiq::Stats.new
|
|
257
|
-
redis_stats
|
|
309
|
+
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
|
258
310
|
json(
|
|
259
311
|
sidekiq: {
|
|
260
|
-
processed:
|
|
261
|
-
failed:
|
|
262
|
-
busy:
|
|
263
|
-
processes:
|
|
264
|
-
enqueued:
|
|
265
|
-
scheduled:
|
|
266
|
-
retries:
|
|
267
|
-
dead:
|
|
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,
|
|
268
320
|
default_latency: sidekiq_stats.default_queue_latency
|
|
269
321
|
},
|
|
270
322
|
redis: redis_stats,
|
|
@@ -272,60 +324,130 @@ module Sidekiq
|
|
|
272
324
|
)
|
|
273
325
|
end
|
|
274
326
|
|
|
275
|
-
get
|
|
276
|
-
json Sidekiq::Stats
|
|
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
|
|
277
407
|
end
|
|
278
408
|
|
|
279
409
|
def call(env)
|
|
280
410
|
action = self.class.match(env)
|
|
281
|
-
return [404, {
|
|
411
|
+
return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
|
|
282
412
|
|
|
413
|
+
app = @klass
|
|
283
414
|
resp = catch(:halt) do
|
|
284
|
-
app = @klass
|
|
285
415
|
self.class.run_befores(app, action)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
self.class.run_afters(app, action)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
resp
|
|
416
|
+
action.instance_exec env, &action.block
|
|
417
|
+
ensure
|
|
418
|
+
self.class.run_afters(app, action)
|
|
293
419
|
end
|
|
294
420
|
|
|
295
|
-
|
|
421
|
+
case resp
|
|
296
422
|
when Array
|
|
423
|
+
# redirects go here
|
|
297
424
|
resp
|
|
298
425
|
else
|
|
426
|
+
# rendered content goes here
|
|
299
427
|
headers = {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
428
|
+
Rack::CONTENT_TYPE => "text/html",
|
|
429
|
+
Rack::CACHE_CONTROL => "private, no-store",
|
|
430
|
+
Web::CONTENT_LANGUAGE => action.locale,
|
|
431
|
+
Web::CONTENT_SECURITY_POLICY => CSP_HEADER
|
|
304
432
|
}
|
|
305
|
-
|
|
433
|
+
# we'll let Rack calculate Content-Length for us.
|
|
306
434
|
[200, headers, [resp]]
|
|
307
435
|
end
|
|
308
|
-
|
|
309
|
-
resp[1] = resp[1].dup
|
|
310
|
-
|
|
311
|
-
resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
|
|
312
|
-
|
|
313
|
-
resp
|
|
314
436
|
end
|
|
315
437
|
|
|
316
|
-
def self.helpers(mod=nil, &block)
|
|
317
|
-
if
|
|
438
|
+
def self.helpers(mod = nil, &block)
|
|
439
|
+
if block
|
|
318
440
|
WebAction.class_eval(&block)
|
|
319
441
|
else
|
|
320
442
|
WebAction.send(:include, mod)
|
|
321
443
|
end
|
|
322
444
|
end
|
|
323
445
|
|
|
324
|
-
def self.before(path=nil, &block)
|
|
446
|
+
def self.before(path = nil, &block)
|
|
325
447
|
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
|
326
448
|
end
|
|
327
449
|
|
|
328
|
-
def self.after(path=nil, &block)
|
|
450
|
+
def self.after(path = nil, &block)
|
|
329
451
|
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
|
330
452
|
end
|
|
331
453
|
|
|
@@ -338,8 +460,8 @@ module Sidekiq
|
|
|
338
460
|
end
|
|
339
461
|
|
|
340
462
|
def self.run_hooks(hooks, app, action)
|
|
341
|
-
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
|
342
|
-
|
|
463
|
+
hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
|
464
|
+
.each { |_, b| action.instance_exec(action.env, app, &b) }
|
|
343
465
|
end
|
|
344
466
|
|
|
345
467
|
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
|