sidekiq 4.2.10 → 6.1.2
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 +5 -5
- data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +2 -1
- data/.standard.yml +20 -0
- data/5.0-Upgrade.md +56 -0
- data/6.0-Upgrade.md +72 -0
- data/COMM-LICENSE +12 -10
- data/Changes.md +354 -1
- data/Ent-2.0-Upgrade.md +37 -0
- data/Ent-Changes.md +111 -3
- data/Gemfile +16 -21
- data/Gemfile.lock +192 -0
- data/LICENSE +1 -1
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-5.0-Upgrade.md +25 -0
- data/Pro-Changes.md +181 -4
- data/README.md +19 -33
- data/Rakefile +6 -8
- data/bin/sidekiq +26 -2
- data/bin/sidekiqload +37 -34
- data/bin/sidekiqmon +8 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
- data/lib/generators/sidekiq/worker_generator.rb +21 -13
- data/lib/sidekiq.rb +86 -61
- data/lib/sidekiq/api.rb +320 -209
- data/lib/sidekiq/cli.rb +207 -217
- data/lib/sidekiq/client.rb +78 -51
- data/lib/sidekiq/delay.rb +41 -0
- data/lib/sidekiq/exception_handler.rb +12 -16
- data/lib/sidekiq/extensions/action_mailer.rb +13 -22
- data/lib/sidekiq/extensions/active_record.rb +13 -10
- data/lib/sidekiq/extensions/class_methods.rb +14 -11
- data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
- data/lib/sidekiq/fetch.rb +29 -30
- data/lib/sidekiq/job_logger.rb +63 -0
- data/lib/sidekiq/job_retry.rb +262 -0
- data/lib/sidekiq/launcher.rb +102 -69
- data/lib/sidekiq/logger.rb +165 -0
- data/lib/sidekiq/manager.rb +16 -19
- data/lib/sidekiq/middleware/chain.rb +15 -5
- data/lib/sidekiq/middleware/i18n.rb +5 -7
- data/lib/sidekiq/monitor.rb +133 -0
- data/lib/sidekiq/paginator.rb +18 -14
- data/lib/sidekiq/processor.rb +161 -82
- data/lib/sidekiq/rails.rb +27 -100
- data/lib/sidekiq/redis_connection.rb +60 -20
- data/lib/sidekiq/scheduled.rb +61 -35
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +24 -0
- data/lib/sidekiq/testing.rb +48 -28
- data/lib/sidekiq/testing/inline.rb +2 -1
- data/lib/sidekiq/util.rb +20 -16
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +57 -57
- data/lib/sidekiq/web/action.rb +14 -14
- data/lib/sidekiq/web/application.rb +103 -84
- data/lib/sidekiq/web/csrf_protection.rb +158 -0
- data/lib/sidekiq/web/helpers.rb +126 -71
- data/lib/sidekiq/web/router.rb +18 -17
- data/lib/sidekiq/worker.rb +164 -41
- data/sidekiq.gemspec +15 -27
- data/web/assets/javascripts/application.js +25 -27
- data/web/assets/javascripts/dashboard.js +33 -37
- data/web/assets/stylesheets/application-dark.css +143 -0
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +385 -10
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +2 -2
- data/web/locales/ar.yml +81 -0
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +4 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +2 -2
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +9 -4
- data/web/locales/lt.yml +83 -0
- data/web/locales/pl.yml +4 -4
- data/web/locales/ru.yml +4 -0
- data/web/locales/ur.yml +80 -0
- data/web/locales/vi.yml +83 -0
- data/web/views/_footer.erb +5 -2
- data/web/views/_job_info.erb +2 -1
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +15 -8
- data/web/views/dashboard.erb +1 -1
- data/web/views/dead.erb +2 -2
- data/web/views/layout.erb +12 -2
- data/web/views/morgue.erb +9 -6
- data/web/views/queue.erb +18 -8
- data/web/views/queues.erb +11 -1
- data/web/views/retries.erb +14 -7
- data/web/views/retry.erb +2 -2
- data/web/views/scheduled.erb +7 -4
- metadata +41 -188
- data/.github/issue_template.md +0 -9
- data/.travis.yml +0 -18
- data/bin/sidekiqctl +0 -99
- data/lib/sidekiq/core_ext.rb +0 -119
- data/lib/sidekiq/logging.rb +0 -106
- data/lib/sidekiq/middleware/server/active_record.rb +0 -13
- data/lib/sidekiq/middleware/server/logging.rb +0 -31
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
data/lib/sidekiq/web/action.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
class WebAction
|
5
|
-
RACK_SESSION =
|
5
|
+
RACK_SESSION = "rack.session"
|
6
6
|
|
7
7
|
attr_accessor :env, :block, :type
|
8
8
|
|
@@ -19,14 +19,14 @@ module Sidekiq
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def redirect(location)
|
22
|
-
throw :halt, [302, {
|
22
|
+
throw :halt, [302, {"Location" => "#{request.base_url}#{location}"}, []]
|
23
23
|
end
|
24
24
|
|
25
25
|
def params
|
26
|
-
indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
26
|
+
indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
|
27
27
|
|
28
28
|
indifferent_hash.merge! request.params
|
29
|
-
route_params.each {|k,v| indifferent_hash[k.to_s] = v }
|
29
|
+
route_params.each { |k, v| indifferent_hash[k.to_s] = v }
|
30
30
|
|
31
31
|
indifferent_hash
|
32
32
|
end
|
@@ -39,15 +39,15 @@ module Sidekiq
|
|
39
39
|
env[RACK_SESSION]
|
40
40
|
end
|
41
41
|
|
42
|
-
def content_type(type)
|
43
|
-
@type = type
|
44
|
-
end
|
45
|
-
|
46
42
|
def erb(content, options = {})
|
47
|
-
if content.
|
43
|
+
if content.is_a? Symbol
|
48
44
|
unless respond_to?(:"_erb_#{content}")
|
49
45
|
src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
|
50
|
-
WebAction.class_eval
|
46
|
+
WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
+
def _erb_#{content}
|
48
|
+
#{src}
|
49
|
+
end
|
50
|
+
RUBY
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
@@ -68,22 +68,22 @@ module Sidekiq
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def json(payload)
|
71
|
-
[200, {
|
71
|
+
[200, {"Content-Type" => "application/json", "Cache-Control" => "no-cache"}, [Sidekiq.dump_json(payload)]]
|
72
72
|
end
|
73
73
|
|
74
74
|
def initialize(env, block)
|
75
75
|
@_erb = false
|
76
76
|
@env = env
|
77
77
|
@block = block
|
78
|
-
|
78
|
+
@files ||= {}
|
79
79
|
end
|
80
80
|
|
81
81
|
private
|
82
82
|
|
83
83
|
def _erb(file, locals)
|
84
|
-
locals
|
84
|
+
locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
|
85
85
|
|
86
|
-
if file.
|
86
|
+
if file.is_a?(String)
|
87
87
|
ERB.new(file).result(binding)
|
88
88
|
else
|
89
89
|
send(:"_erb_#{file}")
|
@@ -4,9 +4,23 @@ module Sidekiq
|
|
4
4
|
class WebApplication
|
5
5
|
extend WebRouter
|
6
6
|
|
7
|
-
CONTENT_LENGTH = "Content-Length"
|
8
|
-
|
9
|
-
|
7
|
+
CONTENT_LENGTH = "Content-Length"
|
8
|
+
REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
|
9
|
+
CSP_HEADER = [
|
10
|
+
"default-src 'self' https: http:",
|
11
|
+
"child-src 'self'",
|
12
|
+
"connect-src 'self' https: http: wss: ws:",
|
13
|
+
"font-src 'self' https: http:",
|
14
|
+
"frame-src 'self'",
|
15
|
+
"img-src 'self' https: http: data:",
|
16
|
+
"manifest-src 'self'",
|
17
|
+
"media-src 'self'",
|
18
|
+
"object-src 'none'",
|
19
|
+
"script-src 'self' https: http: 'unsafe-inline'",
|
20
|
+
"style-src 'self' https: http: 'unsafe-inline'",
|
21
|
+
"worker-src 'self'",
|
22
|
+
"base-uri 'self'"
|
23
|
+
].join("; ").freeze
|
10
24
|
|
11
25
|
def initialize(klass)
|
12
26
|
@klass = klass
|
@@ -29,8 +43,8 @@ module Sidekiq
|
|
29
43
|
end
|
30
44
|
|
31
45
|
get "/" do
|
32
|
-
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
33
|
-
stats_history = Sidekiq::Stats::History.new((params[
|
46
|
+
@redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
47
|
+
stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)
|
34
48
|
@processed_history = stats_history.processed
|
35
49
|
@failed_history = stats_history.failed
|
36
50
|
|
@@ -42,14 +56,14 @@ module Sidekiq
|
|
42
56
|
end
|
43
57
|
|
44
58
|
post "/busy" do
|
45
|
-
if params[
|
46
|
-
p = Sidekiq::Process.new(
|
47
|
-
p.quiet! if params[
|
48
|
-
p.stop! if params[
|
59
|
+
if params["identity"]
|
60
|
+
p = Sidekiq::Process.new("identity" => params["identity"])
|
61
|
+
p.quiet! if params["quiet"]
|
62
|
+
p.stop! if params["stop"]
|
49
63
|
else
|
50
64
|
processes.each do |pro|
|
51
|
-
pro.quiet! if params[
|
52
|
-
pro.stop! if params[
|
65
|
+
pro.quiet! if params["quiet"]
|
66
|
+
pro.stop! if params["stop"]
|
53
67
|
end
|
54
68
|
end
|
55
69
|
|
@@ -67,37 +81,46 @@ module Sidekiq
|
|
67
81
|
|
68
82
|
halt(404) unless @name
|
69
83
|
|
70
|
-
@count = (params[
|
84
|
+
@count = (params["count"] || 25).to_i
|
71
85
|
@queue = Sidekiq::Queue.new(@name)
|
72
|
-
(@current_page, @total_size, @messages) = page("queue:#{@name}", params[
|
86
|
+
(@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
|
73
87
|
@messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
|
74
88
|
|
75
89
|
erb(:queue)
|
76
90
|
end
|
77
91
|
|
78
92
|
post "/queues/:name" do
|
79
|
-
Sidekiq::Queue.new(route_params[:name])
|
93
|
+
queue = Sidekiq::Queue.new(route_params[:name])
|
94
|
+
|
95
|
+
if Sidekiq.pro? && params["pause"]
|
96
|
+
queue.pause!
|
97
|
+
elsif Sidekiq.pro? && params["unpause"]
|
98
|
+
queue.unpause!
|
99
|
+
else
|
100
|
+
queue.clear
|
101
|
+
end
|
80
102
|
|
81
103
|
redirect "#{root_path}queues"
|
82
104
|
end
|
83
105
|
|
84
106
|
post "/queues/:name/delete" do
|
85
107
|
name = route_params[:name]
|
86
|
-
Sidekiq::Job.new(params[
|
108
|
+
Sidekiq::Job.new(params["key_val"], name).delete
|
87
109
|
|
88
110
|
redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
|
89
111
|
end
|
90
112
|
|
91
|
-
get
|
92
|
-
@count = (params[
|
93
|
-
(@current_page, @total_size, @dead) = page("dead", params[
|
113
|
+
get "/morgue" do
|
114
|
+
@count = (params["count"] || 25).to_i
|
115
|
+
(@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
|
94
116
|
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
95
117
|
|
96
118
|
erb(:morgue)
|
97
119
|
end
|
98
120
|
|
99
121
|
get "/morgue/:key" do
|
100
|
-
|
122
|
+
key = route_params[:key]
|
123
|
+
halt(404) unless key
|
101
124
|
|
102
125
|
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
103
126
|
|
@@ -108,10 +131,10 @@ module Sidekiq
|
|
108
131
|
end
|
109
132
|
end
|
110
133
|
|
111
|
-
post
|
112
|
-
redirect(request.path) unless params[
|
134
|
+
post "/morgue" do
|
135
|
+
redirect(request.path) unless params["key"]
|
113
136
|
|
114
|
-
params[
|
137
|
+
params["key"].each do |key|
|
115
138
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
116
139
|
retry_or_delete_or_kill job, params if job
|
117
140
|
end
|
@@ -132,7 +155,8 @@ module Sidekiq
|
|
132
155
|
end
|
133
156
|
|
134
157
|
post "/morgue/:key" do
|
135
|
-
|
158
|
+
key = route_params[:key]
|
159
|
+
halt(404) unless key
|
136
160
|
|
137
161
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
138
162
|
retry_or_delete_or_kill job, params if job
|
@@ -140,9 +164,9 @@ module Sidekiq
|
|
140
164
|
redirect_with_query("#{root_path}morgue")
|
141
165
|
end
|
142
166
|
|
143
|
-
get
|
144
|
-
@count = (params[
|
145
|
-
(@current_page, @total_size, @retries) = page("retry", params[
|
167
|
+
get "/retries" do
|
168
|
+
@count = (params["count"] || 25).to_i
|
169
|
+
(@current_page, @total_size, @retries) = page("retry", params["page"], @count)
|
146
170
|
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
147
171
|
|
148
172
|
erb(:retries)
|
@@ -158,10 +182,10 @@ module Sidekiq
|
|
158
182
|
end
|
159
183
|
end
|
160
184
|
|
161
|
-
post
|
162
|
-
redirect(request.path) unless params[
|
185
|
+
post "/retries" do
|
186
|
+
redirect(request.path) unless params["key"]
|
163
187
|
|
164
|
-
params[
|
188
|
+
params["key"].each do |key|
|
165
189
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
166
190
|
retry_or_delete_or_kill job, params if job
|
167
191
|
end
|
@@ -181,6 +205,12 @@ module Sidekiq
|
|
181
205
|
redirect "#{root_path}retries"
|
182
206
|
end
|
183
207
|
|
208
|
+
post "/retries/all/kill" do
|
209
|
+
Sidekiq::RetrySet.new.kill_all
|
210
|
+
|
211
|
+
redirect "#{root_path}retries"
|
212
|
+
end
|
213
|
+
|
184
214
|
post "/retries/:key" do
|
185
215
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
186
216
|
|
@@ -189,9 +219,9 @@ module Sidekiq
|
|
189
219
|
redirect_with_query("#{root_path}retries")
|
190
220
|
end
|
191
221
|
|
192
|
-
get
|
193
|
-
@count = (params[
|
194
|
-
(@current_page, @total_size, @scheduled) = page("schedule", params[
|
222
|
+
get "/scheduled" do
|
223
|
+
@count = (params["count"] || 25).to_i
|
224
|
+
(@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
|
195
225
|
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
196
226
|
|
197
227
|
erb(:scheduled)
|
@@ -207,10 +237,10 @@ module Sidekiq
|
|
207
237
|
end
|
208
238
|
end
|
209
239
|
|
210
|
-
post
|
211
|
-
redirect(request.path) unless params[
|
240
|
+
post "/scheduled" do
|
241
|
+
redirect(request.path) unless params["key"]
|
212
242
|
|
213
|
-
params[
|
243
|
+
params["key"].each do |key|
|
214
244
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
215
245
|
delete_or_add_queue job, params if job
|
216
246
|
end
|
@@ -219,7 +249,8 @@ module Sidekiq
|
|
219
249
|
end
|
220
250
|
|
221
251
|
post "/scheduled/:key" do
|
222
|
-
|
252
|
+
key = route_params[:key]
|
253
|
+
halt(404) unless key
|
223
254
|
|
224
255
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
225
256
|
delete_or_add_queue job, params if job
|
@@ -227,76 +258,64 @@ module Sidekiq
|
|
227
258
|
redirect_with_query("#{root_path}scheduled")
|
228
259
|
end
|
229
260
|
|
230
|
-
get
|
261
|
+
get "/dashboard/stats" do
|
231
262
|
redirect "#{root_path}stats"
|
232
263
|
end
|
233
264
|
|
234
|
-
get
|
265
|
+
get "/stats" do
|
235
266
|
sidekiq_stats = Sidekiq::Stats.new
|
236
|
-
redis_stats
|
237
|
-
|
267
|
+
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
238
268
|
json(
|
239
269
|
sidekiq: {
|
240
|
-
processed:
|
241
|
-
failed:
|
242
|
-
busy:
|
243
|
-
processes:
|
244
|
-
enqueued:
|
245
|
-
scheduled:
|
246
|
-
retries:
|
247
|
-
dead:
|
270
|
+
processed: sidekiq_stats.processed,
|
271
|
+
failed: sidekiq_stats.failed,
|
272
|
+
busy: sidekiq_stats.workers_size,
|
273
|
+
processes: sidekiq_stats.processes_size,
|
274
|
+
enqueued: sidekiq_stats.enqueued,
|
275
|
+
scheduled: sidekiq_stats.scheduled_size,
|
276
|
+
retries: sidekiq_stats.retry_size,
|
277
|
+
dead: sidekiq_stats.dead_size,
|
248
278
|
default_latency: sidekiq_stats.default_queue_latency
|
249
279
|
},
|
250
|
-
redis: redis_stats
|
280
|
+
redis: redis_stats,
|
281
|
+
server_utc_time: server_utc_time
|
251
282
|
)
|
252
283
|
end
|
253
284
|
|
254
|
-
get
|
285
|
+
get "/stats/queues" do
|
255
286
|
json Sidekiq::Stats::Queues.new.lengths
|
256
287
|
end
|
257
288
|
|
258
289
|
def call(env)
|
259
290
|
action = self.class.match(env)
|
260
|
-
return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"
|
291
|
+
return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action
|
261
292
|
|
262
|
-
|
263
|
-
|
293
|
+
app = @klass
|
294
|
+
resp = catch(:halt) do # rubocop:disable Standard/SemanticBlocks
|
264
295
|
self.class.run_befores(app, action)
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
self.class.run_afters(app, action)
|
269
|
-
end
|
270
|
-
|
271
|
-
resp
|
296
|
+
action.instance_exec env, &action.block
|
297
|
+
ensure
|
298
|
+
self.class.run_afters(app, action)
|
272
299
|
end
|
273
300
|
|
274
|
-
|
301
|
+
case resp
|
275
302
|
when Array
|
303
|
+
# redirects go here
|
276
304
|
resp
|
277
|
-
when Integer
|
278
|
-
[resp, {}, []]
|
279
305
|
else
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
[200, type_header, [resp]]
|
306
|
+
# rendered content goes here
|
307
|
+
headers = {
|
308
|
+
"Content-Type" => "text/html",
|
309
|
+
"Cache-Control" => "no-cache",
|
310
|
+
"Content-Language" => action.locale,
|
311
|
+
"Content-Security-Policy" => CSP_HEADER
|
312
|
+
}
|
313
|
+
# we'll let Rack calculate Content-Length for us.
|
314
|
+
[200, headers, [resp]]
|
290
315
|
end
|
291
|
-
|
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
|
297
316
|
end
|
298
317
|
|
299
|
-
def self.helpers(mod=nil, &block)
|
318
|
+
def self.helpers(mod = nil, &block)
|
300
319
|
if block_given?
|
301
320
|
WebAction.class_eval(&block)
|
302
321
|
else
|
@@ -304,11 +323,11 @@ module Sidekiq
|
|
304
323
|
end
|
305
324
|
end
|
306
325
|
|
307
|
-
def self.before(path=nil, &block)
|
326
|
+
def self.before(path = nil, &block)
|
308
327
|
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
309
328
|
end
|
310
329
|
|
311
|
-
def self.after(path=nil, &block)
|
330
|
+
def self.after(path = nil, &block)
|
312
331
|
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
313
332
|
end
|
314
333
|
|
@@ -321,8 +340,8 @@ module Sidekiq
|
|
321
340
|
end
|
322
341
|
|
323
342
|
def self.run_hooks(hooks, app, action)
|
324
|
-
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
325
|
-
|
343
|
+
hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
344
|
+
.each { |_, b| action.instance_exec(action.env, app, &b) }
|
326
345
|
end
|
327
346
|
|
328
347
|
def self.befores
|
@@ -0,0 +1,158 @@
|
|
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("you need to set up a session middleware *before* #{self.class}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def accept?(env)
|
73
|
+
return true if safe?(env)
|
74
|
+
|
75
|
+
giventoken = ::Rack::Request.new(env).params["authenticity_token"]
|
76
|
+
valid_token?(env, giventoken)
|
77
|
+
end
|
78
|
+
|
79
|
+
TOKEN_LENGTH = 32
|
80
|
+
|
81
|
+
# Checks that the token given to us as a parameter matches
|
82
|
+
# the token stored in the session.
|
83
|
+
def valid_token?(env, giventoken)
|
84
|
+
return false if giventoken.nil? || giventoken.empty?
|
85
|
+
|
86
|
+
begin
|
87
|
+
token = decode_token(giventoken)
|
88
|
+
rescue ArgumentError # client input is invalid
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
|
92
|
+
sess = session(env)
|
93
|
+
|
94
|
+
# Checks that Rack::Session::Cookie did not return empty session
|
95
|
+
# object in case the digest verification failed
|
96
|
+
return false if sess.empty?
|
97
|
+
|
98
|
+
localtoken = sess[:csrf]
|
99
|
+
|
100
|
+
# Rotate the session token after every use
|
101
|
+
sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
|
102
|
+
|
103
|
+
# See if it's actually a masked token or not. We should be able
|
104
|
+
# to handle any unmasked tokens that we've issued without error.
|
105
|
+
|
106
|
+
if unmasked_token?(token)
|
107
|
+
compare_with_real_token token, localtoken
|
108
|
+
elsif masked_token?(token)
|
109
|
+
unmasked = unmask_token(token)
|
110
|
+
compare_with_real_token unmasked, localtoken
|
111
|
+
else
|
112
|
+
false # Token is malformed
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Creates a masked version of the authenticity token that varies
|
117
|
+
# on each request. The masking is used to mitigate SSL attacks
|
118
|
+
# like BREACH.
|
119
|
+
def mask_token(token)
|
120
|
+
token = decode_token(token)
|
121
|
+
one_time_pad = SecureRandom.random_bytes(token.length)
|
122
|
+
encrypted_token = xor_byte_strings(one_time_pad, token)
|
123
|
+
masked_token = one_time_pad + encrypted_token
|
124
|
+
Base64.strict_encode64(masked_token)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Essentially the inverse of +mask_token+.
|
128
|
+
def unmask_token(masked_token)
|
129
|
+
# Split the token into the one-time pad and the encrypted
|
130
|
+
# value and decrypt it
|
131
|
+
token_length = masked_token.length / 2
|
132
|
+
one_time_pad = masked_token[0...token_length]
|
133
|
+
encrypted_token = masked_token[token_length..-1]
|
134
|
+
xor_byte_strings(one_time_pad, encrypted_token)
|
135
|
+
end
|
136
|
+
|
137
|
+
def unmasked_token?(token)
|
138
|
+
token.length == TOKEN_LENGTH
|
139
|
+
end
|
140
|
+
|
141
|
+
def masked_token?(token)
|
142
|
+
token.length == TOKEN_LENGTH * 2
|
143
|
+
end
|
144
|
+
|
145
|
+
def compare_with_real_token(token, local)
|
146
|
+
::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
|
147
|
+
end
|
148
|
+
|
149
|
+
def decode_token(token)
|
150
|
+
Base64.strict_decode64(token)
|
151
|
+
end
|
152
|
+
|
153
|
+
def xor_byte_strings(s1, s2)
|
154
|
+
s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|