sidekiq 5.2.6 → 7.1.0
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.
- checksums.yaml +4 -4
- data/Changes.md +537 -8
- data/LICENSE.txt +9 -0
- data/README.md +47 -50
- 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 +556 -351
- data/lib/sidekiq/capsule.rb +127 -0
- data/lib/sidekiq/cli.rb +203 -226
- data/lib/sidekiq/client.rb +121 -101
- data/lib/sidekiq/component.rb +68 -0
- data/lib/sidekiq/config.rb +274 -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 +131 -108
- data/lib/sidekiq/job_util.rb +105 -0
- data/lib/sidekiq/launcher.rb +203 -105
- data/lib/sidekiq/logger.rb +131 -0
- data/lib/sidekiq/manager.rb +43 -46
- data/lib/sidekiq/metrics/query.rb +153 -0
- data/lib/sidekiq/metrics/shared.rb +95 -0
- data/lib/sidekiq/metrics/tracking.rb +136 -0
- data/lib/sidekiq/middleware/chain.rb +113 -56
- data/lib/sidekiq/middleware/current_attributes.rb +56 -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 +108 -107
- data/lib/sidekiq/rails.rb +49 -38
- data/lib/sidekiq/redis_client_adapter.rb +96 -0
- data/lib/sidekiq/redis_connection.rb +38 -107
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +111 -49
- 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 +66 -84
- data/lib/sidekiq/transaction_aware_client.rb +44 -0
- data/lib/sidekiq/version.rb +3 -1
- data/lib/sidekiq/web/action.rb +15 -11
- data/lib/sidekiq/web/application.rb +123 -79
- data/lib/sidekiq/web/csrf_protection.rb +180 -0
- data/lib/sidekiq/web/helpers.rb +137 -106
- data/lib/sidekiq/web/router.rb +23 -19
- data/lib/sidekiq/web.rb +56 -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 +130 -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 +166 -0
- data/web/assets/javascripts/dashboard.js +36 -292
- data/web/assets/javascripts/metrics.js +264 -0
- data/web/assets/stylesheets/application-dark.css +147 -0
- data/web/assets/stylesheets/application-rtl.css +2 -95
- data/web/assets/stylesheets/application.css +102 -522
- 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 +84 -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 +63 -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 +6 -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 +75 -25
- data/web/views/dashboard.erb +58 -18
- data/web/views/dead.erb +3 -3
- data/web/views/layout.erb +3 -1
- data/web/views/metrics.erb +82 -0
- data/web/views/metrics_for_job.erb +68 -0
- data/web/views/morgue.erb +14 -15
- data/web/views/queue.erb +33 -24
- data/web/views/queues.erb +13 -3
- data/web/views/retries.erb +16 -17
- data/web/views/retry.erb +3 -3
- data/web/views/scheduled.erb +17 -15
- metadata +69 -69
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -11
- data/.gitignore +0 -15
- data/.travis.yml +0 -11
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/5.0-Upgrade.md +0 -56
- data/COMM-LICENSE +0 -97
- data/Ent-Changes.md +0 -238
- data/Gemfile +0 -23
- 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 -759
- data/Rakefile +0 -9
- data/bin/sidekiqctl +0 -20
- data/code_of_conduct.md +0 -50
- data/lib/generators/sidekiq/worker_generator.rb +0 -49
- data/lib/sidekiq/core_ext.rb +0 -1
- data/lib/sidekiq/ctl.rb +0 -221
- 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 -220
@@ -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'",
|
@@ -21,7 +19,13 @@ module Sidekiq
|
|
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
|
53
|
+
Sidekiq.redis { |c| c.llen("queue:default") }
|
54
|
+
""
|
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,52 @@ 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
|
277
329
|
end
|
278
330
|
|
279
331
|
def call(env)
|
280
332
|
action = self.class.match(env)
|
281
|
-
return [404, {"
|
333
|
+
return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
|
282
334
|
|
335
|
+
app = @klass
|
283
336
|
resp = catch(:halt) do
|
284
|
-
app = @klass
|
285
337
|
self.class.run_befores(app, action)
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
self.class.run_afters(app, action)
|
290
|
-
end
|
291
|
-
|
292
|
-
resp
|
338
|
+
action.instance_exec env, &action.block
|
339
|
+
ensure
|
340
|
+
self.class.run_afters(app, action)
|
293
341
|
end
|
294
342
|
|
295
|
-
|
343
|
+
case resp
|
296
344
|
when Array
|
345
|
+
# redirects go here
|
297
346
|
resp
|
298
347
|
else
|
348
|
+
# rendered content goes here
|
299
349
|
headers = {
|
300
|
-
"
|
301
|
-
"
|
302
|
-
"
|
303
|
-
"
|
350
|
+
"content-type" => "text/html",
|
351
|
+
"cache-control" => "private, no-store",
|
352
|
+
"content-language" => action.locale,
|
353
|
+
"content-security-policy" => CSP_HEADER
|
304
354
|
}
|
305
|
-
|
355
|
+
# we'll let Rack calculate Content-Length for us.
|
306
356
|
[200, headers, [resp]]
|
307
357
|
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
358
|
end
|
315
359
|
|
316
|
-
def self.helpers(mod=nil, &block)
|
317
|
-
if
|
360
|
+
def self.helpers(mod = nil, &block)
|
361
|
+
if block
|
318
362
|
WebAction.class_eval(&block)
|
319
363
|
else
|
320
364
|
WebAction.send(:include, mod)
|
321
365
|
end
|
322
366
|
end
|
323
367
|
|
324
|
-
def self.before(path=nil, &block)
|
368
|
+
def self.before(path = nil, &block)
|
325
369
|
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
326
370
|
end
|
327
371
|
|
328
|
-
def self.after(path=nil, &block)
|
372
|
+
def self.after(path = nil, &block)
|
329
373
|
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
330
374
|
end
|
331
375
|
|
@@ -338,8 +382,8 @@ module Sidekiq
|
|
338
382
|
end
|
339
383
|
|
340
384
|
def self.run_hooks(hooks, app, action)
|
341
|
-
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
342
|
-
|
385
|
+
hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
386
|
+
.each { |_, b| action.instance_exec(action.env, app, &b) }
|
343
387
|
end
|
344
388
|
|
345
389
|
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.urlsafe_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..]
|
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.urlsafe_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
|