sidekiq 4.2.2 → 6.3.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 +516 -0
- data/LICENSE +2 -2
- data/README.md +23 -36
- 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 +401 -243
- data/lib/sidekiq/cli.rb +228 -212
- data/lib/sidekiq/client.rb +76 -53
- 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 +12 -4
- data/lib/sidekiq/fetch.rb +39 -31
- data/lib/sidekiq/job.rb +13 -0
- data/lib/sidekiq/job_logger.rb +63 -0
- data/lib/sidekiq/job_retry.rb +259 -0
- data/lib/sidekiq/launcher.rb +170 -71
- data/lib/sidekiq/logger.rb +166 -0
- data/lib/sidekiq/manager.rb +17 -20
- data/lib/sidekiq/middleware/chain.rb +20 -8
- data/lib/sidekiq/middleware/current_attributes.rb +52 -0
- 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 +169 -78
- data/lib/sidekiq/rails.rb +41 -36
- data/lib/sidekiq/redis_connection.rb +65 -20
- data/lib/sidekiq/scheduled.rb +85 -34
- 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 +52 -26
- data/lib/sidekiq/util.rb +48 -15
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +15 -17
- data/lib/sidekiq/web/application.rb +114 -92
- data/lib/sidekiq/web/csrf_protection.rb +180 -0
- data/lib/sidekiq/web/helpers.rb +151 -83
- data/lib/sidekiq/web/router.rb +27 -19
- data/lib/sidekiq/web.rb +85 -76
- data/lib/sidekiq/worker.rb +233 -43
- data/lib/sidekiq.rb +88 -64
- data/sidekiq.gemspec +24 -22
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +86 -59
- data/web/assets/javascripts/dashboard.js +81 -85
- data/web/assets/stylesheets/application-dark.css +147 -0
- data/web/assets/stylesheets/application-rtl.css +242 -0
- data/web/assets/stylesheets/application.css +319 -141
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +2 -2
- data/web/locales/ar.yml +87 -0
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +8 -1
- data/web/locales/es.yml +22 -5
- data/web/locales/fa.yml +80 -0
- data/web/locales/fr.yml +10 -3
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +12 -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 +4 -3
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/_poll_link.erb +2 -5
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +60 -22
- data/web/views/dashboard.erb +23 -15
- data/web/views/dead.erb +3 -3
- data/web/views/layout.erb +14 -3
- data/web/views/morgue.erb +19 -12
- data/web/views/queue.erb +24 -14
- data/web/views/queues.erb +14 -4
- data/web/views/retries.erb +22 -13
- data/web/views/retry.erb +4 -4
- data/web/views/scheduled.erb +7 -4
- metadata +44 -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 -570
- 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 -201
- 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 -50
- 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 -666
- data/test/test_web_helpers.rb +0 -54
data/lib/sidekiq/web/action.rb
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
class WebAction
|
5
|
-
RACK_SESSION =
|
6
|
-
TEXT_HTML = { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }.freeze
|
7
|
-
APPLICATION_JSON = { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }.freeze
|
5
|
+
RACK_SESSION = "rack.session"
|
8
6
|
|
9
7
|
attr_accessor :env, :block, :type
|
10
8
|
|
@@ -17,18 +15,18 @@ module Sidekiq
|
|
17
15
|
end
|
18
16
|
|
19
17
|
def halt(res)
|
20
|
-
throw :halt, res
|
18
|
+
throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
|
21
19
|
end
|
22
20
|
|
23
21
|
def redirect(location)
|
24
|
-
throw :halt, [302, {
|
22
|
+
throw :halt, [302, {"Location" => "#{request.base_url}#{location}"}, []]
|
25
23
|
end
|
26
24
|
|
27
25
|
def params
|
28
|
-
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 }
|
29
27
|
|
30
28
|
indifferent_hash.merge! request.params
|
31
|
-
route_params.each {|k,v| indifferent_hash[k.to_s] = v }
|
29
|
+
route_params.each { |k, v| indifferent_hash[k.to_s] = v }
|
32
30
|
|
33
31
|
indifferent_hash
|
34
32
|
end
|
@@ -41,15 +39,15 @@ module Sidekiq
|
|
41
39
|
env[RACK_SESSION]
|
42
40
|
end
|
43
41
|
|
44
|
-
def content_type(type)
|
45
|
-
@type = type
|
46
|
-
end
|
47
|
-
|
48
42
|
def erb(content, options = {})
|
49
|
-
if content.
|
43
|
+
if content.is_a? Symbol
|
50
44
|
unless respond_to?(:"_erb_#{content}")
|
51
45
|
src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
|
52
|
-
WebAction.class_eval
|
46
|
+
WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
+
def _erb_#{content}
|
48
|
+
#{src}
|
49
|
+
end
|
50
|
+
RUBY
|
53
51
|
end
|
54
52
|
end
|
55
53
|
|
@@ -70,22 +68,22 @@ module Sidekiq
|
|
70
68
|
end
|
71
69
|
|
72
70
|
def json(payload)
|
73
|
-
[200,
|
71
|
+
[200, {"Content-Type" => "application/json", "Cache-Control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
|
74
72
|
end
|
75
73
|
|
76
74
|
def initialize(env, block)
|
77
75
|
@_erb = false
|
78
76
|
@env = env
|
79
77
|
@block = block
|
80
|
-
|
78
|
+
@files ||= {}
|
81
79
|
end
|
82
80
|
|
83
81
|
private
|
84
82
|
|
85
83
|
def _erb(file, locals)
|
86
|
-
locals
|
84
|
+
locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
|
87
85
|
|
88
|
-
if file.
|
86
|
+
if file.is_a?(String)
|
89
87
|
ERB.new(file).result(binding)
|
90
88
|
else
|
91
89
|
send(:"_erb_#{file}")
|
@@ -4,10 +4,22 @@ module Sidekiq
|
|
4
4
|
class WebApplication
|
5
5
|
extend WebRouter
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
11
23
|
|
12
24
|
def initialize(klass)
|
13
25
|
@klass = klass
|
@@ -28,14 +40,17 @@ module Sidekiq
|
|
28
40
|
def self.set(key, val)
|
29
41
|
# nothing, backwards compatibility
|
30
42
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
43
|
+
|
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
|
+
""
|
34
49
|
end
|
35
50
|
|
36
51
|
get "/" do
|
37
|
-
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
38
|
-
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)
|
39
54
|
@processed_history = stats_history.processed
|
40
55
|
@failed_history = stats_history.failed
|
41
56
|
|
@@ -47,14 +62,14 @@ module Sidekiq
|
|
47
62
|
end
|
48
63
|
|
49
64
|
post "/busy" do
|
50
|
-
if params[
|
51
|
-
p = Sidekiq::Process.new(
|
52
|
-
p.quiet! if params[
|
53
|
-
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"]
|
54
69
|
else
|
55
70
|
processes.each do |pro|
|
56
|
-
pro.quiet! if params[
|
57
|
-
pro.stop! if params[
|
71
|
+
pro.quiet! if params["quiet"]
|
72
|
+
pro.stop! if params["stop"]
|
58
73
|
end
|
59
74
|
end
|
60
75
|
|
@@ -67,42 +82,53 @@ module Sidekiq
|
|
67
82
|
erb(:queues)
|
68
83
|
end
|
69
84
|
|
85
|
+
QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
|
86
|
+
|
70
87
|
get "/queues/:name" do
|
71
88
|
@name = route_params[:name]
|
72
89
|
|
73
|
-
halt(404)
|
90
|
+
halt(404) if !@name || @name !~ QUEUE_NAME
|
74
91
|
|
75
|
-
@count = (params[
|
92
|
+
@count = (params["count"] || 25).to_i
|
76
93
|
@queue = Sidekiq::Queue.new(@name)
|
77
|
-
(@current_page, @total_size, @
|
78
|
-
@
|
94
|
+
(@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
|
95
|
+
@jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
|
79
96
|
|
80
97
|
erb(:queue)
|
81
98
|
end
|
82
99
|
|
83
100
|
post "/queues/:name" do
|
84
|
-
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
|
85
110
|
|
86
111
|
redirect "#{root_path}queues"
|
87
112
|
end
|
88
113
|
|
89
114
|
post "/queues/:name/delete" do
|
90
115
|
name = route_params[:name]
|
91
|
-
Sidekiq::
|
116
|
+
Sidekiq::JobRecord.new(params["key_val"], name).delete
|
92
117
|
|
93
|
-
redirect_with_query("#{root_path}queues/#{name}")
|
118
|
+
redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
|
94
119
|
end
|
95
120
|
|
96
|
-
get
|
97
|
-
@count = (params[
|
98
|
-
(@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)
|
99
124
|
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
100
125
|
|
101
126
|
erb(:morgue)
|
102
127
|
end
|
103
128
|
|
104
129
|
get "/morgue/:key" do
|
105
|
-
|
130
|
+
key = route_params[:key]
|
131
|
+
halt(404) unless key
|
106
132
|
|
107
133
|
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
108
134
|
|
@@ -113,10 +139,10 @@ module Sidekiq
|
|
113
139
|
end
|
114
140
|
end
|
115
141
|
|
116
|
-
post
|
117
|
-
redirect(request.path) unless params[
|
142
|
+
post "/morgue" do
|
143
|
+
redirect(request.path) unless params["key"]
|
118
144
|
|
119
|
-
params[
|
145
|
+
params["key"].each do |key|
|
120
146
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
121
147
|
retry_or_delete_or_kill job, params if job
|
122
148
|
end
|
@@ -137,7 +163,8 @@ module Sidekiq
|
|
137
163
|
end
|
138
164
|
|
139
165
|
post "/morgue/:key" do
|
140
|
-
|
166
|
+
key = route_params[:key]
|
167
|
+
halt(404) unless key
|
141
168
|
|
142
169
|
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
143
170
|
retry_or_delete_or_kill job, params if job
|
@@ -145,9 +172,9 @@ module Sidekiq
|
|
145
172
|
redirect_with_query("#{root_path}morgue")
|
146
173
|
end
|
147
174
|
|
148
|
-
get
|
149
|
-
@count = (params[
|
150
|
-
(@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)
|
151
178
|
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
152
179
|
|
153
180
|
erb(:retries)
|
@@ -163,10 +190,10 @@ module Sidekiq
|
|
163
190
|
end
|
164
191
|
end
|
165
192
|
|
166
|
-
post
|
167
|
-
redirect(request.path) unless params[
|
193
|
+
post "/retries" do
|
194
|
+
redirect(request.path) unless params["key"]
|
168
195
|
|
169
|
-
params[
|
196
|
+
params["key"].each do |key|
|
170
197
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
171
198
|
retry_or_delete_or_kill job, params if job
|
172
199
|
end
|
@@ -186,6 +213,12 @@ module Sidekiq
|
|
186
213
|
redirect "#{root_path}retries"
|
187
214
|
end
|
188
215
|
|
216
|
+
post "/retries/all/kill" do
|
217
|
+
Sidekiq::RetrySet.new.kill_all
|
218
|
+
|
219
|
+
redirect "#{root_path}retries"
|
220
|
+
end
|
221
|
+
|
189
222
|
post "/retries/:key" do
|
190
223
|
job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
191
224
|
|
@@ -194,9 +227,9 @@ module Sidekiq
|
|
194
227
|
redirect_with_query("#{root_path}retries")
|
195
228
|
end
|
196
229
|
|
197
|
-
get
|
198
|
-
@count = (params[
|
199
|
-
(@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)
|
200
233
|
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
201
234
|
|
202
235
|
erb(:scheduled)
|
@@ -212,10 +245,10 @@ module Sidekiq
|
|
212
245
|
end
|
213
246
|
end
|
214
247
|
|
215
|
-
post
|
216
|
-
redirect(request.path) unless params[
|
248
|
+
post "/scheduled" do
|
249
|
+
redirect(request.path) unless params["key"]
|
217
250
|
|
218
|
-
params[
|
251
|
+
params["key"].each do |key|
|
219
252
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
220
253
|
delete_or_add_queue job, params if job
|
221
254
|
end
|
@@ -224,7 +257,8 @@ module Sidekiq
|
|
224
257
|
end
|
225
258
|
|
226
259
|
post "/scheduled/:key" do
|
227
|
-
|
260
|
+
key = route_params[:key]
|
261
|
+
halt(404) unless key
|
228
262
|
|
229
263
|
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
230
264
|
delete_or_add_queue job, params if job
|
@@ -232,88 +266,76 @@ module Sidekiq
|
|
232
266
|
redirect_with_query("#{root_path}scheduled")
|
233
267
|
end
|
234
268
|
|
235
|
-
get
|
269
|
+
get "/dashboard/stats" do
|
236
270
|
redirect "#{root_path}stats"
|
237
271
|
end
|
238
272
|
|
239
|
-
get
|
273
|
+
get "/stats" do
|
240
274
|
sidekiq_stats = Sidekiq::Stats.new
|
241
|
-
redis_stats
|
242
|
-
|
275
|
+
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
243
276
|
json(
|
244
277
|
sidekiq: {
|
245
|
-
processed:
|
246
|
-
failed:
|
247
|
-
busy:
|
248
|
-
processes:
|
249
|
-
enqueued:
|
250
|
-
scheduled:
|
251
|
-
retries:
|
252
|
-
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,
|
253
286
|
default_latency: sidekiq_stats.default_queue_latency
|
254
287
|
},
|
255
|
-
redis: redis_stats
|
288
|
+
redis: redis_stats,
|
289
|
+
server_utc_time: server_utc_time
|
256
290
|
)
|
257
291
|
end
|
258
292
|
|
259
|
-
get
|
293
|
+
get "/stats/queues" do
|
260
294
|
json Sidekiq::Stats::Queues.new.lengths
|
261
295
|
end
|
262
296
|
|
263
297
|
def call(env)
|
264
298
|
action = self.class.match(env)
|
265
|
-
return
|
299
|
+
return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action
|
266
300
|
|
267
|
-
|
268
|
-
|
301
|
+
app = @klass
|
302
|
+
resp = catch(:halt) do # rubocop:disable Standard/SemanticBlocks
|
269
303
|
self.class.run_befores(app, action)
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
self.class.run_afters(app, action)
|
274
|
-
end
|
275
|
-
|
276
|
-
resp
|
304
|
+
action.instance_exec env, &action.block
|
305
|
+
ensure
|
306
|
+
self.class.run_afters(app, action)
|
277
307
|
end
|
278
308
|
|
279
|
-
|
309
|
+
case resp
|
280
310
|
when Array
|
311
|
+
# redirects go here
|
281
312
|
resp
|
282
|
-
when Fixnum
|
283
|
-
[resp, {}, []]
|
284
313
|
else
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
[200, type_header, [resp]]
|
314
|
+
# rendered content goes here
|
315
|
+
headers = {
|
316
|
+
"Content-Type" => "text/html",
|
317
|
+
"Cache-Control" => "private, no-store",
|
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]]
|
295
323
|
end
|
296
|
-
|
297
|
-
resp[1] = resp[1].dup
|
298
|
-
|
299
|
-
resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
|
300
|
-
|
301
|
-
resp
|
302
324
|
end
|
303
325
|
|
304
|
-
def self.helpers(mod=nil, &block)
|
305
|
-
if
|
326
|
+
def self.helpers(mod = nil, &block)
|
327
|
+
if block
|
306
328
|
WebAction.class_eval(&block)
|
307
329
|
else
|
308
330
|
WebAction.send(:include, mod)
|
309
331
|
end
|
310
332
|
end
|
311
333
|
|
312
|
-
def self.before(path=nil, &block)
|
334
|
+
def self.before(path = nil, &block)
|
313
335
|
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
314
336
|
end
|
315
337
|
|
316
|
-
def self.after(path=nil, &block)
|
338
|
+
def self.after(path = nil, &block)
|
317
339
|
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
318
340
|
end
|
319
341
|
|
@@ -326,8 +348,8 @@ module Sidekiq
|
|
326
348
|
end
|
327
349
|
|
328
350
|
def self.run_hooks(hooks, app, action)
|
329
|
-
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
330
|
-
|
351
|
+
hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
|
352
|
+
.each { |_, b| action.instance_exec(action.env, app, &b) }
|
331
353
|
end
|
332
354
|
|
333
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
|