sqreen 1.13.4 → 1.14.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67667de7c101298b439e12b54ab0a2572dce27cec1e60237e9baa54b2f3791fb
4
- data.tar.gz: b689401bd7b0bc4731d93a511e458f1aec7bd0257c7f318090278962597be245
3
+ metadata.gz: be5e0fc0157660b4143e23279952f83b846a5c85b922b6bcdb6740f88ec1a6b1
4
+ data.tar.gz: 3b07af02a64839b8028d783831c42f7a46af9faeaed32d535ad7092ca1e667ee
5
5
  SHA512:
6
- metadata.gz: c2b627b8d752c78eb16a9a0a9ea050b0bf6c59b2a99dd86568c30d8926bf10d60f9ebc62ee241267c187c47f2621bae275f5bcce7ffb15779ae42570df61de51
7
- data.tar.gz: c272f746230f351ce74f3fd6d7b7118cf4dcebf0f6f0fc5a38a64cedbfbfee07f21c9d5f577d1c36a9b8264b4bbb92d06ea99cd9776ff47a35415f6f5791e2f0
6
+ metadata.gz: 8a5cae395aa6f18212e296675641be3f8e55237b808346deed258496b8eb6597323146702cd2e34531c6154e8faee6d974610e3a054069ef815b915f97c32950
7
+ data.tar.gz: cf0e72d2bf2e976bed8e29dd3f3c9f1c718efdc55490745e550e5163ea16f33e7bccee3e9169944bd669d1d6a4ab679ecf705ca22febfbb0ebd07a21702ffc9b
@@ -0,0 +1,25 @@
1
+ require 'execjs'
2
+
3
+ module Sqreen
4
+ module Js
5
+ class ExecjsAdapter < JsServiceAdapter
6
+ def preprocess(_rule_name, code)
7
+ ExecJsRunnable.new(ExecJS.compile(code))
8
+ end
9
+
10
+ def variant_name
11
+ ExecJS.runtime.name + ' (ExecJS)'
12
+ end
13
+ end
14
+
15
+ class ExecJsRunnable < ExecutableJs
16
+ def initialize(compiled)
17
+ @compiled = compiled
18
+ end
19
+
20
+ def run_js_cb(cbname, _budget, arguments)
21
+ @compiled.call(cbname, *arguments)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,133 @@
1
+ require 'sqreen/exception'
2
+
3
+ module Sqreen
4
+ module Js
5
+ # start public interface
6
+
7
+ class JsService
8
+ include Singleton
9
+
10
+ def initialize
11
+ detect_adapter
12
+ end
13
+
14
+ # @return [Sqreen::Js::ExecutableJs]
15
+ def prepare(rule_name, code)
16
+ raise 'Not online' unless online?
17
+ @adapter.preprocess(rule_name, code)
18
+ end
19
+
20
+ def online?
21
+ @online
22
+ end
23
+
24
+ def variant
25
+ @adapter.variant_name
26
+ end
27
+
28
+ private
29
+
30
+ def detect_adapter
31
+ if Sqreen.features['isolated_mini_racer'] || ENV['SQREEN_FORCE_SQ_MINI_RACER']
32
+ @online = try_sq_mini_racer
33
+ else
34
+ @online = try_mini_racer || try_rhino || try_therubyracer
35
+ end
36
+
37
+ Sqreen.log.info "JS engine online: #{variant}" if @online
38
+ end
39
+
40
+ def try_mini_racer
41
+ gem = Gem.loaded_specs['mini_racer']
42
+ unless gem
43
+ Sqreen.log.info "mini_racer gem not detected"
44
+ return false
45
+ end
46
+ unless gem.version >= Gem::Version.create('0.1.15') &&
47
+ gem.version < Gem::Version.create('2.0')
48
+ Sqreen.log.warn 'mini_racer won\'t be used because its version is' \
49
+ "#{gem.version}; not >= 0.1.15, < 2.0"
50
+ return false
51
+ end
52
+
53
+ require 'mini_racer'
54
+ require 'sqreen/js/mini_racer_adapter'
55
+ @adapter = MiniRacerAdapter.new
56
+ rescue LoadError => e
57
+ Sqreen.log.warn "Failed loading mini_racer: #{e}"
58
+ false
59
+ end
60
+
61
+ def try_sq_mini_racer
62
+ gem = Gem.loaded_specs['sq_mini_racer']
63
+ unless gem
64
+ Sqreen.log.info "sq_mini_racer gem not detected"
65
+ return false
66
+ end
67
+
68
+ require 'sqreen/mini_racer'
69
+ require 'sqreen/js/mini_racer_adapter'
70
+ @adapter = MiniRacerAdapter.new(true)
71
+ rescue LoadError => e
72
+ Sqreen.log.warn "Failed loading sq_mini_racer: #{e}"
73
+ false
74
+ end
75
+
76
+ def try_rhino
77
+ gem = Gem.loaded_specs['therubyrhino']
78
+ unless gem
79
+ Sqreen.log.info "therubyrhino gem not detected"
80
+ return false
81
+ end
82
+
83
+ require 'rhino'
84
+ require 'sqreen/js/execjs_adapter'
85
+ @adapter = ExecjsAdapter.new
86
+ rescue LoadError => e
87
+ Sqreen.log.warn "Failed loading rhino: #{e}"
88
+ false
89
+ end
90
+
91
+ def try_therubyracer
92
+ gem = Gem.loaded_specs['therubyracer']
93
+ unless gem
94
+ Sqreen.log.info "therubyracer gem not detected"
95
+ return false
96
+ end
97
+ unless gem.version >= Gem::Version.create('0.12.1')
98
+ Sqreen.log.warn 'therubyracer won\'t be used because its version is ' \
99
+ "#{gem.version}; not >= 0.12.1"
100
+ return false
101
+ end
102
+
103
+ require 'therubyracer'
104
+ require 'sqreen/js/execjs_adapter'
105
+ @adapter = ExecjsAdapter.new
106
+ rescue LoadError => e
107
+ Sqreen.log.warn "Failed loading therubyracer: #{e}"
108
+ false
109
+ end
110
+
111
+ end
112
+
113
+ CallContext = Struct.new(:inst, :args, :rv)
114
+
115
+ class ExecutableJs
116
+ def run_js_cb(_cb_name, _budget, _arguments)
117
+ raise Sqreen::NotImplementedYet
118
+ end
119
+ end
120
+
121
+ # end public interface
122
+
123
+ class JsServiceAdapter
124
+ def preprocess(code)
125
+ raise Sqreen::NotImplementedYet
126
+ end
127
+
128
+ def variant_name
129
+ 'unspecified'
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,118 @@
1
+ require 'weakref'
2
+
3
+ module Sqreen
4
+ module Js
5
+ GC_MINI_RACER = 10_000
6
+
7
+ class MiniRacerAdapter < JsServiceAdapter
8
+ def initialize(vendored = false)
9
+ @vendored = vendored
10
+ end
11
+
12
+ def preprocess(_rule_name, code)
13
+ MiniRacerExecutableJs.new(code, @vendored)
14
+ end
15
+
16
+ def variant_name
17
+ @vendored ? 'sq_mini_racer' : 'mini_racer'
18
+ end
19
+ end
20
+
21
+ # Auxiliary classes
22
+
23
+ class MiniRacerExecutableJs < ExecutableJs
24
+ @@ctx_defined = false
25
+
26
+ def initialize(source, vendored)
27
+ @module = vendored ? Sqreen::MiniRacer : MiniRacer
28
+ @source = source
29
+ @recycle_runtime_every = GC_MINI_RACER
30
+ @snapshot = @module::Snapshot.new(source)
31
+ @runtimes = []
32
+ @tl_key = "SQREEN_MINI_RACER_CONTEXT_#{object_id}".freeze
33
+ unless @@ctx_defined
34
+ self.class.define_sqreen_context(@module)
35
+ @@ctx_defined = true
36
+ end
37
+ end
38
+
39
+ def run_js_cb(cb_name, budget, arguments)
40
+ mini_racer_context = Thread.current[@tl_key]
41
+ dead_runtime = !mini_racer_context ||
42
+ !mini_racer_context[:r] || !mini_racer_context[:r].weakref_alive?
43
+ if !dead_runtime && mini_racer_context[:c] >= @recycle_runtime_every
44
+ dispose_runtime(mini_racer_context[:r])
45
+ dead_runtime = true
46
+ end
47
+ if dead_runtime
48
+ new_runtime = SqreenContext.new(:snapshot => @snapshot)
49
+ push_runtime new_runtime
50
+ mini_racer_context = {
51
+ :c => 0,
52
+ :r => WeakCtx.new(new_runtime),
53
+ }
54
+ Thread.current[@tl_key] = mini_racer_context
55
+ end
56
+
57
+ mini_racer_context[:c] += 1
58
+ begin
59
+ mini_racer_context[:r].eval_unsafe(
60
+ "#{cb_name}.apply(this, #{::JSON.generate(arguments)})", nil, budget)
61
+ rescue @module::ScriptTerminatedError
62
+ nil
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def push_runtime(runtime)
69
+ @runtimes.delete_if do |th, runt, _thid|
70
+ del = th.nil? || !th.weakref_alive? || !th.alive?
71
+ runt.dispose if del
72
+ del
73
+ end
74
+ @runtimes.push [WeakRef.new(Thread.current), runtime, Thread.current.object_id]
75
+ end
76
+
77
+ def dispose_runtime(runtime)
78
+ @runtimes.delete_if { |_th, _runt, thid| thid == Thread.current.object_id }
79
+ runtime.dispose
80
+ end
81
+
82
+ class << self
83
+ def define_sqreen_context(modoole)
84
+ # Context specialized for Sqreen usage
85
+ Sqreen::Js.const_set 'SqreenContext', Class.new(modoole.const_get('Context'))
86
+ SqreenContext.class_eval do
87
+ def eval_unsafe(str, filename = nil, timeoutv = nil)
88
+ # Beware, timeout could be kept in the context
89
+ # if perf cap is removed after having been activated
90
+ # As it's unused by execjscb we are not cleaning it
91
+ return super(str, filename) if timeoutv.nil?
92
+ return if timeoutv <= 0.0
93
+ timeoutv *= 1000 # Timeout are currently expressed in seconds
94
+ @timeout = timeoutv
95
+ @eval_thread = Thread.current
96
+ timeout do
97
+ super(str, filename)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+ # Weak ref to a context
107
+ # enables us to skip a method missing call
108
+ class WeakCtx < WeakRef
109
+ def initialize(*args)
110
+ super(*args)
111
+ end
112
+
113
+ def eval_unsafe(str, filename, timeoutv)
114
+ __getobj__.eval_unsafe(str, filename, timeoutv)
115
+ end
116
+ end
117
+ end
118
+ end
data/lib/sqreen/rules.rb CHANGED
@@ -75,6 +75,17 @@ module Sqreen
75
75
 
76
76
  cb_class = nil
77
77
  js = hash_rule[Attrs::CALLBACKS]
78
+
79
+ if js && !Sqreen::Js::JsService.instance.online?
80
+ unless @@issue_nojs_warn
81
+ @@issue_nojs_warn = true
82
+ Sqreen.log.warn('No JavaScript engine is available. ' \
83
+ 'JavaScript callbacks will be ignored')
84
+ end
85
+ Sqreen.log.info("Ignoring JS callback #{rule_name}")
86
+ return nil
87
+ end
88
+
78
89
  cb_class = ExecJSCB if js
79
90
 
80
91
  if cbname && Rules.const_defined?(cbname)
@@ -1,22 +1,8 @@
1
1
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
2
  # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
3
 
4
- if defined?(::JRUBY_VERSION)
5
- require 'rhino'
6
- SQREEN_MINI_RACER = false
7
- else
8
- begin
9
- require 'mini_racer'
10
- SQREEN_MINI_RACER = true
11
- GC_MINI_RACER = 10_000
12
- rescue LoadError
13
- require 'therubyracer'
14
- SQREEN_MINI_RACER = false
15
- end
16
- end
17
4
 
18
- require 'weakref'
19
- require 'execjs'
5
+ require 'sqreen/js/js_service'
20
6
 
21
7
  require 'sqreen/rule_attributes'
22
8
  require 'sqreen/rule_callback'
@@ -25,64 +11,30 @@ require 'sqreen/binding_accessor'
25
11
  require 'sqreen/events/remote_exception'
26
12
 
27
13
  module Sqreen
28
- if SQREEN_MINI_RACER
29
- # Context specialized for Sqreen usage
30
- class SqreenContext < MiniRacer::Context
31
- def eval_unsafe(str, filename = nil, timeoutv = nil)
32
- # Beware, timeout could be kept in the context
33
- # if perf cap is removed after having been activated
34
- # As it's unused by execjscb we are not cleaning it
35
- return super(str, filename) if timeoutv.nil?
36
- return if timeoutv <= 0.0
37
- timeoutv *= 1000 # Timeout are currently expressed in seconds
38
- @timeout = timeoutv
39
- @eval_thread = Thread.current
40
- timeout do
41
- super(str, filename)
42
- end
43
- end
44
- end
45
-
46
- # Weak ref to a context
47
- # enables us to skip a method missing call
48
- class WeakCtx < WeakRef
49
- def initialize(*args)
50
- super(*args)
51
- end
52
-
53
- def eval_unsafe(str, filename, timeoutv)
54
- __getobj__.eval_unsafe(str, filename, timeoutv)
55
- end
56
- end
57
- end
58
14
  module Rules
59
15
  # Exec js callbacks
60
16
  class ExecJSCB < RuleCB
61
- attr_accessor :restrict_max_depth
62
- attr_reader :runtimes
63
- attr_accessor :recycle_runtime_every
17
+
18
+ class << self
19
+ # @return [Sqreen::Js::JsService]
20
+ def js_service
21
+ Sqreen::Js::JsService.instance
22
+ end
23
+ end
64
24
 
65
25
  def initialize(klass, method, rule_hash)
66
26
  super(klass, method, rule_hash)
67
27
  callbacks = @rule[Attrs::CALLBACKS]
68
28
  @conditions = @rule.fetch(Attrs::CONDITIONS, {})
69
29
 
70
- if callbacks['pre'].nil? &&
71
- callbacks['post'].nil? &&
72
- callbacks['failing'].nil?
73
- raise(Sqreen::Exception, 'no JS CB provided')
74
- end
75
-
76
30
  build_runnable(callbacks)
77
- @restrict_max_depth = 20
78
- unless SQREEN_MINI_RACER
79
- @compiled = ExecJS.compile(@source)
80
- return
31
+
32
+ unless pre? || post? || failing?
33
+ raise Sqreen::Exception, 'no JS CB provided'
81
34
  end
82
- @recycle_runtime_every = GC_MINI_RACER
83
- @snapshot = MiniRacer::Snapshot.new(@source)
84
- @runtimes = []
85
- @key = "SQREEN_MINI_RACER_CONTEXT_#{object_id}".freeze
35
+
36
+ @executable = ExecJSCB.js_service.prepare(rule_name, @source)
37
+ @argument_filter = ArgumentFilter.new(rule_hash)
86
38
  end
87
39
 
88
40
  def pre?
@@ -100,63 +52,22 @@ module Sqreen
100
52
  def pre(inst, args, budget = nil, &_block)
101
53
  return unless pre?
102
54
 
103
- call_callback('pre', inst, budget, args)
55
+ call_callback('pre', budget, inst, @cb_bas['pre'], args)
104
56
  end
105
57
 
106
58
  def post(rv, inst, args, budget = nil, &_block)
107
59
  return unless post?
108
60
 
109
- call_callback('post', inst, budget, args, rv)
61
+ call_callback('post', budget, inst, @cb_bas['post'], args, rv)
110
62
  end
111
63
 
112
64
  def failing(rv, inst, args, budget = nil, &_block)
113
65
  return unless failing?
114
66
 
115
- call_callback('failing', inst, budget, args, rv)
116
- end
117
-
118
- def self.hash_val_included(needed, haystack, min_length = 8, max_depth = 20)
119
- new_obj = {}
120
- insert = []
121
- to_do = haystack.map { |k, v| [new_obj, k, v, 0] }
122
- until to_do.empty?
123
- where, key, value, deepness = to_do.pop
124
- safe_key = key.is_a?(Integer) ? key : key.to_s
125
- if value.is_a?(Hash) && deepness < max_depth
126
- val = {}
127
- insert << [where, safe_key, val]
128
- to_do += value.map { |k, v| [val, k, v, deepness + 1] }
129
- elsif value.is_a?(Array) && deepness < max_depth
130
- val = []
131
- insert << [where, safe_key, val]
132
- i = -1
133
- to_do += value.map { |v| [val, i += 1, v, deepness + 1] }
134
- elsif deepness >= max_depth # if we are after max_depth don't try to filter
135
- insert << [where, safe_key, value]
136
- else
137
- v = value.to_s
138
- if v.size >= min_length && ConditionEvaluator.str_include?(needed.to_s, v)
139
- case where
140
- when Array
141
- where << value
142
- else
143
- where[safe_key] = value
144
- end
145
- end
146
- end
147
- end
148
- insert.reverse.each do |wh, ikey, ival|
149
- case wh
150
- when Array
151
- wh << ival unless ival.respond_to?(:empty?) && ival.empty?
152
- else
153
- wh[ikey] = ival unless ival.respond_to?(:empty?) && ival.empty?
154
- end
155
- end
156
- new_obj
67
+ call_callback('failing', budget, inst, @cb_bas['failing'], args, rv)
157
68
  end
158
69
 
159
- protected
70
+ private
160
71
 
161
72
  def record_and_continue?(ret)
162
73
  case ret
@@ -165,10 +76,10 @@ module Sqreen
165
76
  when Hash
166
77
  ret.keys.each do |k|
167
78
  ret[(begin
168
- k.to_sym
169
- rescue StandardError
170
- k
171
- end)] = ret[k] end
79
+ k.to_sym
80
+ rescue StandardError
81
+ k
82
+ end)] = ret[k] end
172
83
  record_event(ret[:record]) unless ret[:record].nil?
173
84
  unless ret['observations'].nil?
174
85
  ret['observations'].each do |obs|
@@ -182,134 +93,163 @@ module Sqreen
182
93
  end
183
94
  end
184
95
 
185
- def push_runtime(runtime)
186
- @runtimes.delete_if do |th, runt, _thid|
187
- del = th.nil? || !th.weakref_alive? || !th.alive?
188
- runt.dispose if del
189
- del
96
+ def call_callback(cb_name, budget, inst, cb_ba_args, args, rv = nil)
97
+ arguments = cb_ba_args.map do |ba|
98
+ ba.resolve(binding, framework, inst, args, @data, rv)
190
99
  end
191
- @runtimes.push [WeakRef.new(Thread.current), runtime, Thread.current.object_id]
192
- end
100
+ arguments = @argument_filter.filter(cb_name, arguments)
193
101
 
194
- def dispose_runtime(runtime)
195
- @runtimes.delete_if { |_th, _runt, thid| thid == Thread.current.object_id }
196
- runtime.dispose
197
- end
102
+ ret = @executable.run_js_cb(cb_name, budget, arguments)
198
103
 
199
- def call_callback(name, inst, budget, args, rv = nil)
200
- mini_racer_context = nil
201
- if SQREEN_MINI_RACER
202
- mini_racer_context = Thread.current[@key]
203
- dead_runtime = !mini_racer_context || !mini_racer_context[:r] || !mini_racer_context[:r].weakref_alive?
204
- if !dead_runtime && mini_racer_context[:c] >= @recycle_runtime_every
205
- dispose_runtime(mini_racer_context[:r])
206
- dead_runtime = true
207
- end
208
- if dead_runtime
209
- new_runtime = SqreenContext.new(:snapshot => @snapshot)
210
- push_runtime new_runtime
211
- mini_racer_context = {
212
- :c => 0,
213
- :r => WeakCtx.new(new_runtime),
214
- }
215
- Thread.current[@key] = mini_racer_context
216
- end
217
- end
218
- ret = nil
219
- args_override = nil
220
- arguments = nil
221
- while true
222
- arguments = (args_override || @argument_requirements[name]).map do |accessor|
223
- accessor.resolve(binding, framework, inst, args, @data, rv)
224
- end
225
- arguments = restrict(name, arguments) if @conditions.key?(name)
226
- Sqreen.log.debug { [name, arguments].inspect }
227
- if SQREEN_MINI_RACER
228
- mini_racer_context[:c] += 1
229
- begin
230
- ret = mini_racer_context[:r].eval_unsafe("#{name}.apply(this, #{::JSON.generate(arguments)})", nil, budget)
231
- rescue MiniRacer::ScriptTerminatedError
232
- ret = nil
233
- end
234
- else
235
- ret = @compiled.call(name, *arguments)
236
- end
237
- unless record_and_continue?(ret)
238
- return nil if ret.nil?
239
- return advise_action(ret[:status], ret)
240
- end
241
- name = ret[:call]
242
- rv = ret[:data]
243
- args_override = ret[:args]
244
- args_override = build_accessor(args_override) if args_override
104
+ unless record_and_continue?(ret)
105
+ return nil if ret.nil?
106
+ return advise_action(ret[:status], ret)
245
107
  end
108
+
109
+ name = ret[:call]
110
+ rv = ret[:data]
111
+ new_ba_args = if ret[:args]
112
+ self.class.build_accessors(ret[:args])
113
+ else
114
+ @cb_bas[name] || []
115
+ end
116
+
117
+ # XXX: budgets was not subtracted from
118
+ call_callback(name, budget, inst, new_ba_args, args, rv)
119
+
246
120
  rescue StandardError => e
247
- Sqreen.log.warn { "we catch a JScb exception: #{e.inspect}" }
121
+ Sqreen.log.warn { "Caught JS callback exception: #{e.inspect}" }
248
122
  Sqreen.log.debug e.backtrace
249
- record_exception(e, :cb => name, :args => arguments)
123
+ record_exception(e, :cb => cb_name, :args => arguments)
250
124
  nil
251
125
  end
252
126
 
253
- def each_hash_val_include(condition, depth = 10)
254
- return if depth <= 0
255
- condition.each do |key, values|
256
- if key == ConditionEvaluator::HASH_INC_OPERATOR
257
- yield values
127
+
128
+ def self.build_accessors(reqs)
129
+ reqs.map do |req|
130
+ BindingAccessor.new(req, true)
131
+ end
132
+ end
133
+
134
+ def build_runnable(callbacks)
135
+ @cb_bas = {}
136
+ @source = ''
137
+ @js_pre = !callbacks['pre'].nil?
138
+ @js_post = !callbacks['post'].nil?
139
+ @js_failing = !callbacks['failing'].nil?
140
+ callbacks.each do |name, args_or_func|
141
+ @source << "this['#{name.tr("'", "\\'")}'] = "
142
+ if args_or_func.is_a?(Array)
143
+ args_or_func = args_or_func.dup
144
+ @source << args_or_func.pop
145
+ @cb_bas[name] = self.class.build_accessors(args_or_func)
258
146
  else
259
- values.map do |v|
260
- each_hash_val_include(v, depth - 1) { |vals| yield vals } if v.is_a?(Hash)
261
- end
147
+ @source << args_or_func
148
+ @cb_bas[name] = []
262
149
  end
150
+ @source << ";\n"
263
151
  end
264
152
  end
153
+ end
154
+
155
+ class ArgumentFilter
156
+ MAX_DEPTH = 2
157
+
158
+ def initialize(rule)
159
+ @conditions = rule.fetch(Attrs::CONDITIONS, {})
160
+ build_arg_requirements rule
161
+ end
265
162
 
266
- def restrict(cbname, arguments)
163
+ def filter(cbname, arguments)
267
164
  condition = @conditions[cbname]
268
- return arguments if condition.nil? || @argument_requirements[cbname].nil?
165
+ return arguments if condition.nil? || @ba_expressions[cbname].nil?
269
166
 
270
167
  each_hash_val_include(condition) do |needle, haystack, min_length|
271
168
  # We could actually run the binding accessor expression here.
272
- needed_idx = @argument_requirements[cbname].map(&:expression).index(needle)
169
+ needed_idx = @ba_expressions[cbname].index(needle)
273
170
  next unless needed_idx
274
171
 
275
- haystack_idx = @argument_requirements[cbname].map(&:expression).index(haystack)
172
+ haystack_idx = @ba_expressions[cbname].index(haystack)
276
173
  next unless haystack_idx
277
174
 
278
- arguments[haystack_idx] = ExecJSCB.hash_val_included(
279
- arguments[needed_idx],
280
- arguments[haystack_idx],
281
- min_length.to_i,
282
- @restrict_max_depth
175
+ arguments[haystack_idx] = ArgumentFilter.hash_val_included(
176
+ arguments[needed_idx],
177
+ arguments[haystack_idx],
178
+ min_length.to_i,
179
+ MAX_DEPTH
283
180
  )
284
181
  end
285
182
 
286
183
  arguments
287
184
  end
288
185
 
289
- def build_accessor(reqs)
290
- reqs.map do |req|
291
- BindingAccessor.new(req, true)
186
+ def build_arg_requirements(rule)
187
+ @ba_expressions = {}
188
+ callbacks = rule[Attrs::CALLBACKS]
189
+ callbacks.each do |name, args_or_func|
190
+ next unless args_or_func.is_a?(Array)
191
+ args_bas = args_or_func[0..-2] unless args_or_func.empty?
192
+ @ba_expressions[name] =
193
+ ExecJSCB.build_accessors(args_bas).map(&:expression)
292
194
  end
293
195
  end
294
196
 
295
- def build_runnable(callbacks)
296
- @argument_requirements = {}
297
- @source = ''
298
- @js_pre = !callbacks['pre'].nil?
299
- @js_post = !callbacks['post'].nil?
300
- @js_failing = !callbacks['failing'].nil?
301
- callbacks.each do |name, args_or_func|
302
- @source << "var #{name} = "
303
- if args_or_func.is_a?(Array)
304
- @source << args_or_func.pop
305
- @argument_requirements[name] = build_accessor(args_or_func)
197
+ private
198
+
199
+ def each_hash_val_include(condition, depth = 10)
200
+ return if depth <= 0
201
+ condition.each do |key, values|
202
+ if key == ConditionEvaluator::HASH_INC_OPERATOR
203
+ yield values
306
204
  else
307
- @source << args_or_func
308
- @argument_requirements[name] = []
205
+ values.map do |v|
206
+ each_hash_val_include(v, depth - 1) { |vals| yield vals } if v.is_a?(Hash)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ def self.hash_val_included(needed, haystack, min_length = 8, max_depth = 20)
213
+ new_obj = {}
214
+ insert = []
215
+ to_do = haystack.map { |k, v| [new_obj, k, v, 0] }
216
+ until to_do.empty?
217
+ where, key, value, deepness = to_do.pop
218
+ safe_key = key.is_a?(Integer) ? key : key.to_s
219
+ if value.is_a?(Hash) && deepness < max_depth
220
+ val = {}
221
+ insert << [where, safe_key, val]
222
+ to_do += value.map { |k, v| [val, k, v, deepness + 1] }
223
+ elsif value.is_a?(Array) && deepness < max_depth
224
+ val = []
225
+ insert << [where, safe_key, val]
226
+ i = -1
227
+ to_do += value.map { |v| [val, i += 1, v, deepness + 1] }
228
+ elsif deepness >= max_depth # if we are after max_depth don't try to filter
229
+ insert << [where, safe_key, value]
230
+ else
231
+ v = value.to_s
232
+ if v.size >= min_length && ConditionEvaluator.str_include?(needed.to_s, v)
233
+ case where
234
+ when Array
235
+ where << value
236
+ else
237
+ where[safe_key] = value
238
+ end
239
+ end
309
240
  end
310
- @source << ";\n"
311
241
  end
242
+ insert.reverse.each do |wh, ikey, ival|
243
+ case wh
244
+ when Array
245
+ wh << ival unless ival.respond_to?(:empty?) && ival.empty?
246
+ else
247
+ wh[ikey] = ival unless ival.respond_to?(:empty?) && ival.empty?
248
+ end
249
+ end
250
+ new_obj
312
251
  end
313
252
  end
314
253
  end
315
254
  end
255
+
@@ -0,0 +1,315 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ if defined?(::JRUBY_VERSION)
5
+ require 'rhino'
6
+ SQREEN_MINI_RACER = false
7
+ else
8
+ begin
9
+ require 'mini_racer'
10
+ SQREEN_MINI_RACER = true
11
+ GC_MINI_RACER = 10_000
12
+ rescue LoadError
13
+ require 'therubyracer'
14
+ SQREEN_MINI_RACER = false
15
+ end
16
+ end
17
+
18
+ require 'weakref'
19
+ require 'execjs'
20
+
21
+ require 'sqreen/rule_attributes'
22
+ require 'sqreen/rule_callback'
23
+ require 'sqreen/condition_evaluator'
24
+ require 'sqreen/binding_accessor'
25
+ require 'sqreen/events/remote_exception'
26
+
27
+ module Sqreen
28
+ if SQREEN_MINI_RACER
29
+ # Context specialized for Sqreen usage
30
+ class SqreenContext < MiniRacer::Context
31
+ def eval_unsafe(str, filename = nil, timeoutv = nil)
32
+ # Beware, timeout could be kept in the context
33
+ # if perf cap is removed after having been activated
34
+ # As it's unused by execjscb we are not cleaning it
35
+ return super(str, filename) if timeoutv.nil?
36
+ return if timeoutv <= 0.0
37
+ timeoutv *= 1000 # Timeout are currently expressed in seconds
38
+ @timeout = timeoutv
39
+ @eval_thread = Thread.current
40
+ timeout do
41
+ super(str, filename)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Weak ref to a context
47
+ # enables us to skip a method missing call
48
+ class WeakCtx < WeakRef
49
+ def initialize(*args)
50
+ super(*args)
51
+ end
52
+
53
+ def eval_unsafe(str, filename, timeoutv)
54
+ __getobj__.eval_unsafe(str, filename, timeoutv)
55
+ end
56
+ end
57
+ end
58
+ module Rules
59
+ # Exec js callbacks
60
+ class ExecJSCBOld < RuleCB
61
+ attr_accessor :restrict_max_depth
62
+ attr_reader :runtimes
63
+ attr_accessor :recycle_runtime_every
64
+
65
+ def initialize(klass, method, rule_hash)
66
+ super(klass, method, rule_hash)
67
+ callbacks = @rule[Attrs::CALLBACKS]
68
+ @conditions = @rule.fetch(Attrs::CONDITIONS, {})
69
+
70
+ if callbacks['pre'].nil? &&
71
+ callbacks['post'].nil? &&
72
+ callbacks['failing'].nil?
73
+ raise(Sqreen::Exception, 'no JS CB provided')
74
+ end
75
+
76
+ build_runnable(callbacks)
77
+ @restrict_max_depth = 20
78
+ unless SQREEN_MINI_RACER
79
+ @compiled = ExecJS.compile(@source)
80
+ return
81
+ end
82
+ @recycle_runtime_every = GC_MINI_RACER
83
+ @snapshot = MiniRacer::Snapshot.new(@source)
84
+ @runtimes = []
85
+ @key = "SQREEN_MINI_RACER_CONTEXT_#{object_id}".freeze
86
+ end
87
+
88
+ def pre?
89
+ @js_pre
90
+ end
91
+
92
+ def post?
93
+ @js_post
94
+ end
95
+
96
+ def failing?
97
+ @js_failing
98
+ end
99
+
100
+ def pre(inst, args, budget = nil, &_block)
101
+ return unless pre?
102
+
103
+ call_callback('pre', inst, budget, args)
104
+ end
105
+
106
+ def post(rv, inst, args, budget = nil, &_block)
107
+ return unless post?
108
+
109
+ call_callback('post', inst, budget, args, rv)
110
+ end
111
+
112
+ def failing(rv, inst, args, budget = nil, &_block)
113
+ return unless failing?
114
+
115
+ call_callback('failing', inst, budget, args, rv)
116
+ end
117
+
118
+ def self.hash_val_included(needed, haystack, min_length = 8, max_depth = 20)
119
+ new_obj = {}
120
+ insert = []
121
+ to_do = haystack.map { |k, v| [new_obj, k, v, 0] }
122
+ until to_do.empty?
123
+ where, key, value, deepness = to_do.pop
124
+ safe_key = key.is_a?(Integer) ? key : key.to_s
125
+ if value.is_a?(Hash) && deepness < max_depth
126
+ val = {}
127
+ insert << [where, safe_key, val]
128
+ to_do += value.map { |k, v| [val, k, v, deepness + 1] }
129
+ elsif value.is_a?(Array) && deepness < max_depth
130
+ val = []
131
+ insert << [where, safe_key, val]
132
+ i = -1
133
+ to_do += value.map { |v| [val, i += 1, v, deepness + 1] }
134
+ elsif deepness >= max_depth # if we are after max_depth don't try to filter
135
+ insert << [where, safe_key, value]
136
+ else
137
+ v = value.to_s
138
+ if v.size >= min_length && ConditionEvaluator.str_include?(needed.to_s, v)
139
+ case where
140
+ when Array
141
+ where << value
142
+ else
143
+ where[safe_key] = value
144
+ end
145
+ end
146
+ end
147
+ end
148
+ insert.reverse.each do |wh, ikey, ival|
149
+ case wh
150
+ when Array
151
+ wh << ival unless ival.respond_to?(:empty?) && ival.empty?
152
+ else
153
+ wh[ikey] = ival unless ival.respond_to?(:empty?) && ival.empty?
154
+ end
155
+ end
156
+ new_obj
157
+ end
158
+
159
+ protected
160
+
161
+ def record_and_continue?(ret)
162
+ case ret
163
+ when NilClass
164
+ false
165
+ when Hash
166
+ ret.keys.each do |k|
167
+ ret[(begin
168
+ k.to_sym
169
+ rescue StandardError
170
+ k
171
+ end)] = ret[k] end
172
+ record_event(ret[:record]) unless ret[:record].nil?
173
+ unless ret['observations'].nil?
174
+ ret['observations'].each do |obs|
175
+ obs[3] = Time.parse(obs[3]) if obs.size >= 3 && obs[3].is_a?(String)
176
+ record_observation(*obs)
177
+ end
178
+ end
179
+ !ret[:call].nil?
180
+ else
181
+ raise Sqreen::Exception, "Invalid return type #{ret.inspect}"
182
+ end
183
+ end
184
+
185
+ def push_runtime(runtime)
186
+ @runtimes.delete_if do |th, runt, _thid|
187
+ del = th.nil? || !th.weakref_alive? || !th.alive?
188
+ runt.dispose if del
189
+ del
190
+ end
191
+ @runtimes.push [WeakRef.new(Thread.current), runtime, Thread.current.object_id]
192
+ end
193
+
194
+ def dispose_runtime(runtime)
195
+ @runtimes.delete_if { |_th, _runt, thid| thid == Thread.current.object_id }
196
+ runtime.dispose
197
+ end
198
+
199
+ def call_callback(name, inst, budget, args, rv = nil)
200
+ mini_racer_context = nil
201
+ if SQREEN_MINI_RACER
202
+ mini_racer_context = Thread.current[@key]
203
+ dead_runtime = !mini_racer_context || !mini_racer_context[:r] || !mini_racer_context[:r].weakref_alive?
204
+ if !dead_runtime && mini_racer_context[:c] >= @recycle_runtime_every
205
+ dispose_runtime(mini_racer_context[:r])
206
+ dead_runtime = true
207
+ end
208
+ if dead_runtime
209
+ new_runtime = SqreenContext.new(:snapshot => @snapshot)
210
+ push_runtime new_runtime
211
+ mini_racer_context = {
212
+ :c => 0,
213
+ :r => WeakCtx.new(new_runtime),
214
+ }
215
+ Thread.current[@key] = mini_racer_context
216
+ end
217
+ end
218
+ ret = nil
219
+ args_override = nil
220
+ arguments = nil
221
+ while true
222
+ arguments = (args_override || @argument_requirements[name]).map do |accessor|
223
+ accessor.resolve(binding, framework, inst, args, @data, rv)
224
+ end
225
+ arguments = restrict(name, arguments) if @conditions.key?(name)
226
+ Sqreen.log.debug { [name, arguments].inspect }
227
+ if SQREEN_MINI_RACER
228
+ mini_racer_context[:c] += 1
229
+ begin
230
+ ret = mini_racer_context[:r].eval_unsafe("#{name}.apply(this, #{::JSON.generate(arguments)})", nil, budget)
231
+ rescue MiniRacer::ScriptTerminatedError
232
+ ret = nil
233
+ end
234
+ else
235
+ ret = @compiled.call(name, *arguments)
236
+ end
237
+ unless record_and_continue?(ret)
238
+ return nil if ret.nil?
239
+ return advise_action(ret[:status], ret)
240
+ end
241
+ name = ret[:call]
242
+ rv = ret[:data]
243
+ args_override = ret[:args]
244
+ args_override = build_accessor(args_override) if args_override
245
+ end
246
+ rescue StandardError => e
247
+ Sqreen.log.warn { "we catch a JScb exception: #{e.inspect}" }
248
+ Sqreen.log.debug e.backtrace
249
+ record_exception(e, :cb => name, :args => arguments)
250
+ nil
251
+ end
252
+
253
+ def each_hash_val_include(condition, depth = 10)
254
+ return if depth <= 0
255
+ condition.each do |key, values|
256
+ if key == ConditionEvaluator::HASH_INC_OPERATOR
257
+ yield values
258
+ else
259
+ values.map do |v|
260
+ each_hash_val_include(v, depth - 1) { |vals| yield vals } if v.is_a?(Hash)
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ def restrict(cbname, arguments)
267
+ condition = @conditions[cbname]
268
+ return arguments if condition.nil? || @argument_requirements[cbname].nil?
269
+
270
+ each_hash_val_include(condition) do |needle, haystack, min_length|
271
+ # We could actually run the binding accessor expression here.
272
+ needed_idx = @argument_requirements[cbname].map(&:expression).index(needle)
273
+ next unless needed_idx
274
+
275
+ haystack_idx = @argument_requirements[cbname].map(&:expression).index(haystack)
276
+ next unless haystack_idx
277
+
278
+ arguments[haystack_idx] = ExecJSCB.hash_val_included(
279
+ arguments[needed_idx],
280
+ arguments[haystack_idx],
281
+ min_length.to_i,
282
+ @restrict_max_depth
283
+ )
284
+ end
285
+
286
+ arguments
287
+ end
288
+
289
+ def build_accessor(reqs)
290
+ reqs.map do |req|
291
+ BindingAccessor.new(req, true)
292
+ end
293
+ end
294
+
295
+ def build_runnable(callbacks)
296
+ @argument_requirements = {}
297
+ @source = ''
298
+ @js_pre = !callbacks['pre'].nil?
299
+ @js_post = !callbacks['post'].nil?
300
+ @js_failing = !callbacks['failing'].nil?
301
+ callbacks.each do |name, args_or_func|
302
+ @source << "var #{name} = "
303
+ if args_or_func.is_a?(Array)
304
+ @source << args_or_func.pop
305
+ @argument_requirements[name] = build_accessor(args_or_func)
306
+ else
307
+ @source << args_or_func
308
+ @argument_requirements[name] = []
309
+ end
310
+ @source << ";\n"
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
@@ -122,7 +122,11 @@ module Sqreen
122
122
  if escape_html == false &&
123
123
  text.respond_to?(:include?) &&
124
124
  !text.include?('html_escape')
125
- args[0].replace("Sqreen.escape_haml((#{args[0]}))")
125
+ if text.respond_to? :text=
126
+ args[0].text = "Sqreen.escape_haml((#{args[0].text}))"
127
+ else
128
+ args[0].replace("Sqreen.escape_haml((#{args[0]}))")
129
+ end
126
130
  end
127
131
  nil
128
132
  end
@@ -1,5 +1,5 @@
1
1
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
2
  # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
3
  module Sqreen
4
- VERSION = '1.13.4'.freeze
4
+ VERSION = '1.14.0.beta1'.freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqreen
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.4
4
+ version: 1.14.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sqreen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-16 00:00:00.000000000 Z
11
+ date: 2018-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: execjs
@@ -77,6 +77,9 @@ files:
77
77
  - lib/sqreen/frameworks/sinatra.rb
78
78
  - lib/sqreen/frameworks/sqreen_test.rb
79
79
  - lib/sqreen/instrumentation.rb
80
+ - lib/sqreen/js/execjs_adapter.rb
81
+ - lib/sqreen/js/js_service.rb
82
+ - lib/sqreen/js/mini_racer_adapter.rb
80
83
  - lib/sqreen/log.rb
81
84
  - lib/sqreen/metrics.rb
82
85
  - lib/sqreen/metrics/average.rb
@@ -106,6 +109,7 @@ files:
106
109
  - lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb
107
110
  - lib/sqreen/rules_callbacks/custom_error.rb
108
111
  - lib/sqreen/rules_callbacks/execjs.rb
112
+ - lib/sqreen/rules_callbacks/execjs_old.rb
109
113
  - lib/sqreen/rules_callbacks/headers_insert.rb
110
114
  - lib/sqreen/rules_callbacks/inspect_rule.rb
111
115
  - lib/sqreen/rules_callbacks/matcher_rule.rb
@@ -142,12 +146,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
146
  version: '0'
143
147
  required_rubygems_version: !ruby/object:Gem::Requirement
144
148
  requirements:
145
- - - ">="
149
+ - - ">"
146
150
  - !ruby/object:Gem::Version
147
- version: '0'
151
+ version: 1.3.1
148
152
  requirements: []
149
153
  rubyforge_project:
150
- rubygems_version: 2.7.6
154
+ rubygems_version: 2.7.7
151
155
  signing_key:
152
156
  specification_version: 4
153
157
  summary: Sqreen Ruby agent