sqreen 1.13.4 → 1.14.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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