sidekiq 4.2.4 → 6.2.1
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/Changes.md +445 -0
- data/LICENSE +1 -1
- data/README.md +21 -34
- data/bin/sidekiq +26 -2
- data/bin/sidekiqload +28 -38
- 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 +2 -2
- data/lib/generators/sidekiq/worker_generator.rb +21 -13
- data/lib/sidekiq/api.rb +347 -213
- data/lib/sidekiq/cli.rb +221 -212
- data/lib/sidekiq/client.rb +75 -52
- 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 +38 -31
- data/lib/sidekiq/job_logger.rb +63 -0
- data/lib/sidekiq/job_retry.rb +263 -0
- data/lib/sidekiq/launcher.rb +169 -70
- data/lib/sidekiq/logger.rb +166 -0
- data/lib/sidekiq/manager.rb +17 -20
- 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 -70
- data/lib/sidekiq/rails.rb +30 -73
- data/lib/sidekiq/redis_connection.rb +67 -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/inline.rb +2 -1
- data/lib/sidekiq/testing.rb +54 -26
- data/lib/sidekiq/util.rb +48 -15
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +15 -15
- data/lib/sidekiq/web/application.rb +112 -89
- data/lib/sidekiq/web/csrf_protection.rb +180 -0
- data/lib/sidekiq/web/helpers.rb +153 -73
- data/lib/sidekiq/web/router.rb +27 -19
- data/lib/sidekiq/web.rb +64 -109
- data/lib/sidekiq/worker.rb +164 -41
- data/lib/sidekiq.rb +86 -60
- data/sidekiq.gemspec +24 -22
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +25 -27
- data/web/assets/javascripts/dashboard.js +34 -38
- data/web/assets/stylesheets/application-dark.css +160 -0
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +402 -12
- 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 +80 -0
- data/web/locales/fr.yml +3 -3
- 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 +3 -2
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +57 -19
- data/web/views/dashboard.erb +3 -3
- data/web/views/dead.erb +2 -2
- data/web/views/layout.erb +13 -2
- data/web/views/morgue.erb +19 -12
- data/web/views/queue.erb +22 -12
- data/web/views/queues.erb +13 -3
- data/web/views/retries.erb +22 -13
- data/web/views/retry.erb +3 -3
- data/web/views/scheduled.erb +7 -4
- metadata +42 -194
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -4
- data/.gitignore +0 -12
- data/.travis.yml +0 -12
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/COMM-LICENSE +0 -95
- data/Ent-Changes.md +0 -146
- data/Gemfile +0 -29
- data/Pro-2.0-Upgrade.md +0 -138
- data/Pro-3.0-Upgrade.md +0 -44
- data/Pro-Changes.md +0 -585
- data/Rakefile +0 -9
- data/bin/sidekiqctl +0 -99
- data/code_of_conduct.md +0 -50
- data/lib/sidekiq/core_ext.rb +0 -106
- 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 -40
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
- data/test/config.yml +0 -9
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -1
- data/test/fixtures/en.yml +0 -2
- data/test/helper.rb +0 -75
- data/test/test_actors.rb +0 -138
- data/test/test_api.rb +0 -528
- data/test/test_cli.rb +0 -418
- data/test/test_client.rb +0 -266
- data/test/test_exception_handler.rb +0 -56
- data/test/test_extensions.rb +0 -127
- data/test/test_fetch.rb +0 -50
- data/test/test_launcher.rb +0 -95
- data/test/test_logging.rb +0 -35
- data/test/test_manager.rb +0 -50
- data/test/test_middleware.rb +0 -158
- data/test/test_processor.rb +0 -235
- data/test/test_rails.rb +0 -22
- data/test/test_redis_connection.rb +0 -132
- data/test/test_retry.rb +0 -326
- data/test/test_retry_exhausted.rb +0 -149
- data/test/test_scheduled.rb +0 -115
- data/test/test_scheduling.rb +0 -58
- data/test/test_sidekiq.rb +0 -107
- data/test/test_testing.rb +0 -143
- data/test/test_testing_fake.rb +0 -357
- data/test/test_testing_inline.rb +0 -94
- data/test/test_util.rb +0 -13
- data/test/test_web.rb +0 -726
- data/test/test_web_helpers.rb +0 -54
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
|
|
@@ -15,18 +15,18 @@ module Sidekiq
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def halt(res)
|
18
|
-
throw :halt, res
|
18
|
+
throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
|
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,22 @@ module Sidekiq
|
|
4
4
|
class WebApplication
|
5
5
|
extend WebRouter
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
|
8
|
+
CSP_HEADER = [
|
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' https: http: 'unsafe-inline'",
|
19
|
+
"style-src 'self' https: http: 'unsafe-inline'",
|
20
|
+
"worker-src 'self'",
|
21
|
+
"base-uri 'self'"
|
22
|
+
].join("; ").freeze
|
10
23
|
|
11
24
|
def initialize(klass)
|
12
25
|
@klass = klass
|
@@ -28,13 +41,16 @@ module Sidekiq
|
|
28
41
|
# nothing, backwards compatibility
|
29
42
|
end
|
30
43
|
|
31
|
-
|
32
|
-
|
44
|
+
head "/" do
|
45
|
+
# HEAD / is the cheapest heartbeat possible,
|
46
|
+
# it hits Redis to ensure connectivity
|
47
|
+
Sidekiq.redis { |c| c.llen("queue:default") }
|
48
|
+
""
|
33
49
|
end
|
34
50
|
|
35
51
|
get "/" do
|
36
|
-
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
37
|
-
stats_history = Sidekiq::Stats::History.new((params[
|
52
|
+
@redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
53
|
+
stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)
|
38
54
|
@processed_history = stats_history.processed
|
39
55
|
@failed_history = stats_history.failed
|
40
56
|
|
@@ -46,14 +62,14 @@ module Sidekiq
|
|
46
62
|
end
|
47
63
|
|
48
64
|
post "/busy" do
|
49
|
-
if params[
|
50
|
-
p = Sidekiq::Process.new(
|
51
|
-
p.quiet! if params[
|
52
|
-
p.stop! if params[
|
65
|
+
if params["identity"]
|
66
|
+
p = Sidekiq::Process.new("identity" => params["identity"])
|
67
|
+
p.quiet! if params["quiet"]
|
68
|
+
p.stop! if params["stop"]
|
53
69
|
else
|
54
70
|
processes.each do |pro|
|
55
|
-
pro.quiet! if params[
|
56
|
-
pro.stop! if params[
|
71
|
+
pro.quiet! if params["quiet"]
|
72
|
+
pro.stop! if params["stop"]
|
57
73
|
end
|
58
74
|
end
|
59
75
|
|
@@ -66,42 +82,53 @@ module Sidekiq
|
|
66
82
|
erb(:queues)
|
67
83
|
end
|
68
84
|
|
85
|
+
QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
|
86
|
+
|
69
87
|
get "/queues/:name" do
|
70
88
|
@name = route_params[:name]
|
71
89
|
|
72
|
-
halt(404)
|
90
|
+
halt(404) if !@name || @name !~ QUEUE_NAME
|
73
91
|
|
74
|
-
@count = (params[
|
92
|
+
@count = (params["count"] || 25).to_i
|
75
93
|
@queue = Sidekiq::Queue.new(@name)
|
76
|
-
(@current_page, @total_size, @messages) = page("queue:#{@name}", params[
|
94
|
+
(@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
|
77
95
|
@messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
|
78
96
|
|
79
97
|
erb(:queue)
|
80
98
|
end
|
81
99
|
|
82
100
|
post "/queues/:name" do
|
83
|
-
Sidekiq::Queue.new(route_params[:name])
|
101
|
+
queue = Sidekiq::Queue.new(route_params[:name])
|
102
|
+
|
103
|
+
if Sidekiq.pro? && params["pause"]
|
104
|
+
queue.pause!
|
105
|
+
elsif Sidekiq.pro? && params["unpause"]
|
106
|
+
queue.unpause!
|
107
|
+
else
|
108
|
+
queue.clear
|
109
|
+
end
|
84
110
|
|
85
111
|
redirect "#{root_path}queues"
|
86
112
|
end
|
87
113
|
|
88
114
|
post "/queues/:name/delete" do
|
89
115
|
name = route_params[:name]
|
90
|
-
Sidekiq::Job.new(params[
|
116
|
+
Sidekiq::Job.new(params["key_val"], name).delete
|
91
117
|
|
92
|
-
redirect_with_query("#{root_path}queues/#{name}")
|
118
|
+
redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
|
93
119
|
end
|
94
120
|
|
95
|
-
get
|
96
|
-
@count = (params[
|
97
|
-
(@current_page, @total_size, @dead) = page("dead", params[
|
121
|
+
get "/morgue" do
|
122
|
+
@count = (params["count"] || 25).to_i
|
123
|
+
(@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
|
98
124
|
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
99
125
|
|
100
126
|
erb(:morgue)
|
101
127
|
end
|
102
128
|
|
103
129
|
get "/morgue/:key" do
|
104
|
-
|
130
|
+
key = route_params[:key]
|
131
|
+
halt(404) unless key
|
105
132
|
|
106
133
|
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
107
134
|
|
@@ -112,10 +139,10 @@ module Sidekiq
|
|
112
139
|
end
|
113
140
|
end
|
114
141
|
|
115
|
-
post
|
116
|
-
redirect(request.path) unless params[
|
142
|
+
post "/morgue" do
|
143
|
+
redirect(request.path) unless params["key"]
|
117
144
|
|
118
|
-
params[
|
145
|
+
params["key"].each do |key|
|
119
146
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
120
147
|
retry_or_delete_or_kill job, params if job
|
121
148
|
end
|
@@ -136,7 +163,8 @@ module Sidekiq
|
|
136
163
|
end
|
137
164
|
|
138
165
|
post "/morgue/:key" do
|
139
|
-
|
166
|
+
key = route_params[:key]
|
167
|
+
halt(404) unless key
|
140
168
|
|
141
169
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
142
170
|
retry_or_delete_or_kill job, params if job
|
@@ -144,9 +172,9 @@ module Sidekiq
|
|
144
172
|
redirect_with_query("#{root_path}morgue")
|
145
173
|
end
|
146
174
|
|
147
|
-
get
|
148
|
-
@count = (params[
|
149
|
-
(@current_page, @total_size, @retries) = page("retry", params[
|
175
|
+
get "/retries" do
|
176
|
+
@count = (params["count"] || 25).to_i
|
177
|
+
(@current_page, @total_size, @retries) = page("retry", params["page"], @count)
|
150
178
|
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
151
179
|
|
152
180
|
erb(:retries)
|
@@ -162,10 +190,10 @@ module Sidekiq
|
|
162
190
|
end
|
163
191
|
end
|
164
192
|
|
165
|
-
post
|
166
|
-
redirect(request.path) unless params[
|
193
|
+
post "/retries" do
|
194
|
+
redirect(request.path) unless params["key"]
|
167
195
|
|
168
|
-
params[
|
196
|
+
params["key"].each do |key|
|
169
197
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
170
198
|
retry_or_delete_or_kill job, params if job
|
171
199
|
end
|
@@ -185,6 +213,12 @@ module Sidekiq
|
|
185
213
|
redirect "#{root_path}retries"
|
186
214
|
end
|
187
215
|
|
216
|
+
post "/retries/all/kill" do
|
217
|
+
Sidekiq::RetrySet.new.kill_all
|
218
|
+
|
219
|
+
redirect "#{root_path}retries"
|
220
|
+
end
|
221
|
+
|
188
222
|
post "/retries/:key" do
|
189
223
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
190
224
|
|
@@ -193,9 +227,9 @@ module Sidekiq
|
|
193
227
|
redirect_with_query("#{root_path}retries")
|
194
228
|
end
|
195
229
|
|
196
|
-
get
|
197
|
-
@count = (params[
|
198
|
-
(@current_page, @total_size, @scheduled) = page("schedule", params[
|
230
|
+
get "/scheduled" do
|
231
|
+
@count = (params["count"] || 25).to_i
|
232
|
+
(@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
|
199
233
|
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
200
234
|
|
201
235
|
erb(:scheduled)
|
@@ -211,10 +245,10 @@ module Sidekiq
|
|
211
245
|
end
|
212
246
|
end
|
213
247
|
|
214
|
-
post
|
215
|
-
redirect(request.path) unless params[
|
248
|
+
post "/scheduled" do
|
249
|
+
redirect(request.path) unless params["key"]
|
216
250
|
|
217
|
-
params[
|
251
|
+
params["key"].each do |key|
|
218
252
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
219
253
|
delete_or_add_queue job, params if job
|
220
254
|
end
|
@@ -223,7 +257,8 @@ module Sidekiq
|
|
223
257
|
end
|
224
258
|
|
225
259
|
post "/scheduled/:key" do
|
226
|
-
|
260
|
+
key = route_params[:key]
|
261
|
+
halt(404) unless key
|
227
262
|
|
228
263
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
229
264
|
delete_or_add_queue job, params if job
|
@@ -231,88 +266,76 @@ module Sidekiq
|
|
231
266
|
redirect_with_query("#{root_path}scheduled")
|
232
267
|
end
|
233
268
|
|
234
|
-
get
|
269
|
+
get "/dashboard/stats" do
|
235
270
|
redirect "#{root_path}stats"
|
236
271
|
end
|
237
272
|
|
238
|
-
get
|
273
|
+
get "/stats" do
|
239
274
|
sidekiq_stats = Sidekiq::Stats.new
|
240
|
-
redis_stats
|
241
|
-
|
275
|
+
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
242
276
|
json(
|
243
277
|
sidekiq: {
|
244
|
-
processed:
|
245
|
-
failed:
|
246
|
-
busy:
|
247
|
-
processes:
|
248
|
-
enqueued:
|
249
|
-
scheduled:
|
250
|
-
retries:
|
251
|
-
dead:
|
278
|
+
processed: sidekiq_stats.processed,
|
279
|
+
failed: sidekiq_stats.failed,
|
280
|
+
busy: sidekiq_stats.workers_size,
|
281
|
+
processes: sidekiq_stats.processes_size,
|
282
|
+
enqueued: sidekiq_stats.enqueued,
|
283
|
+
scheduled: sidekiq_stats.scheduled_size,
|
284
|
+
retries: sidekiq_stats.retry_size,
|
285
|
+
dead: sidekiq_stats.dead_size,
|
252
286
|
default_latency: sidekiq_stats.default_queue_latency
|
253
287
|
},
|
254
|
-
redis: redis_stats
|
288
|
+
redis: redis_stats,
|
289
|
+
server_utc_time: server_utc_time
|
255
290
|
)
|
256
291
|
end
|
257
292
|
|
258
|
-
get
|
293
|
+
get "/stats/queues" do
|
259
294
|
json Sidekiq::Stats::Queues.new.lengths
|
260
295
|
end
|
261
296
|
|
262
297
|
def call(env)
|
263
298
|
action = self.class.match(env)
|
264
|
-
return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"
|
299
|
+
return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action
|
265
300
|
|
266
|
-
|
267
|
-
|
301
|
+
app = @klass
|
302
|
+
resp = catch(:halt) do # rubocop:disable Standard/SemanticBlocks
|
268
303
|
self.class.run_befores(app, action)
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
self.class.run_afters(app, action)
|
273
|
-
end
|
274
|
-
|
275
|
-
resp
|
304
|
+
action.instance_exec env, &action.block
|
305
|
+
ensure
|
306
|
+
self.class.run_afters(app, action)
|
276
307
|
end
|
277
308
|
|
278
|
-
|
309
|
+
case resp
|
279
310
|
when Array
|
311
|
+
# redirects go here
|
280
312
|
resp
|
281
|
-
when Fixnum
|
282
|
-
[resp, {}, []]
|
283
313
|
else
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
[200, type_header, [resp]]
|
314
|
+
# rendered content goes here
|
315
|
+
headers = {
|
316
|
+
"Content-Type" => "text/html",
|
317
|
+
"Cache-Control" => "no-cache",
|
318
|
+
"Content-Language" => action.locale,
|
319
|
+
"Content-Security-Policy" => CSP_HEADER
|
320
|
+
}
|
321
|
+
# we'll let Rack calculate Content-Length for us.
|
322
|
+
[200, headers, [resp]]
|
294
323
|
end
|
295
|
-
|
296
|
-
resp[1] = resp[1].dup
|
297
|
-
|
298
|
-
resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
|
299
|
-
|
300
|
-
resp
|
301
324
|
end
|
302
325
|
|
303
|
-
def self.helpers(mod=nil, &block)
|
304
|
-
if
|
326
|
+
def self.helpers(mod = nil, &block)
|
327
|
+
if block
|
305
328
|
WebAction.class_eval(&block)
|
306
329
|
else
|
307
330
|
WebAction.send(:include, mod)
|
308
331
|
end
|
309
332
|
end
|
310
333
|
|
311
|
-
def self.before(path=nil, &block)
|
334
|
+
def self.before(path = nil, &block)
|
312
335
|
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
313
336
|
end
|
314
337
|
|
315
|
-
def self.after(path=nil, &block)
|
338
|
+
def self.after(path = nil, &block)
|
316
339
|
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
317
340
|
end
|
318
341
|
|
@@ -325,8 +348,8 @@ module Sidekiq
|
|
325
348
|
end
|
326
349
|
|
327
350
|
def self.run_hooks(hooks, app, action)
|
328
|
-
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
329
|
-
|
351
|
+
hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
352
|
+
.each { |_, b| action.instance_exec(action.env, app, &b) }
|
330
353
|
end
|
331
354
|
|
332
355
|
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.strict_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..-1]
|
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.strict_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
|