mirror of
https://github.com/rubyjs/therubyrhino
synced 2023-03-27 23:21:34 -04:00
Merge pull request #14 from kares/master
context timeout restriction and 1.9 compatibility
This commit is contained in:
commit
0dd3f7e0ce
9 changed files with 314 additions and 45 deletions
6
.travis.yml
Normal file
6
.travis.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
rvm:
|
||||
- jruby-18mode
|
||||
- jruby-19mode
|
||||
branches:
|
||||
only:
|
||||
- master
|
62
README.rdoc
62
README.rdoc
|
@ -47,18 +47,18 @@ Embed the Mozilla Rhino Javascript interpreter into Ruby
|
|||
|
||||
Rhino::Context.open do |context|
|
||||
context["math"] = MyMath.new
|
||||
context.eval("math.plus(20,22)") #=> 42
|
||||
context.eval("math.plus(20, 22)") #=> 42
|
||||
end
|
||||
|
||||
# make a ruby object *be* your javascript environment
|
||||
math = MyMath.new
|
||||
Rhino::Context.open(:with => math) do |context|
|
||||
context.eval("plus(20,22)") #=> 42
|
||||
context.eval("plus(20, 22)") #=> 42
|
||||
end
|
||||
|
||||
#or the equivalent
|
||||
|
||||
math.eval_js("plus(20,22)")
|
||||
math.eval_js("plus(20, 22)")
|
||||
|
||||
# Configure your embedding setup
|
||||
|
||||
|
@ -69,14 +69,25 @@ Embed the Mozilla Rhino Javascript interpreter into Ruby
|
|||
|
||||
#Turn on Java integration from javascript (probably a bad idea)
|
||||
Rhino::Context.open(:java => true) do |context|
|
||||
context.eval("java.lang.System.exit()") #it's dangerous!
|
||||
context.eval("java.lang.System.exit()") # it's dangerous!
|
||||
end
|
||||
|
||||
#limit the number of instructions that can be executed in order to prevent
|
||||
#rogue scripts
|
||||
Rhino::Context.open do |context|
|
||||
Rhino::Context.open(:restrictable => true) do |context|
|
||||
context.instruction_limit = 100000
|
||||
context.eval("while (true);") # => Error!
|
||||
context.eval("while (true);") # => Rhino::RunawayScriptError
|
||||
end
|
||||
|
||||
#limit the time a script executes
|
||||
#rogue scripts
|
||||
Rhino::Context.open(:restrictable => true, :java => true) do |context|
|
||||
context.timeout_limit = 1.5 # seconds
|
||||
context.eval %Q{
|
||||
for (var i = 0; i < 100; i++) {
|
||||
java.lang.Thread.sleep(100);
|
||||
}
|
||||
} # => Rhino::ScriptTimeoutError
|
||||
end
|
||||
|
||||
==== Different ways of loading javascript source
|
||||
|
@ -93,6 +104,43 @@ In addition to just evaluating strings, you can also use streams such as files.
|
|||
context.load("mysource.js")
|
||||
end
|
||||
|
||||
==== Configurable Ruby access
|
||||
|
||||
By default accessing Ruby objects from javascript is compatible with therubyracer:
|
||||
https://github.com/cowboyd/therubyracer/wiki/Accessing-Ruby-Objects-From-JavaScript
|
||||
|
||||
Thus you end-up calling arbitrary no-arg methods as if they were javascript properties,
|
||||
since instance accessors (properties) and methods (functions) are indistinguishable:
|
||||
|
||||
Rhino::Context.open do |context|
|
||||
context['Time'] = Time
|
||||
context.eval('Time.now')
|
||||
end
|
||||
|
||||
However, you can customize this behavior and there's another access implementation
|
||||
that attempts to mirror only attributes as properties as close as possible:
|
||||
|
||||
class Foo
|
||||
attr_accessor :bar
|
||||
|
||||
def initialize
|
||||
@bar = "bar"
|
||||
end
|
||||
|
||||
def check_bar
|
||||
bar == "bar"
|
||||
end
|
||||
end
|
||||
|
||||
Rhino::Ruby::Scriptable.access = Rhino::Ruby::AttributeAccess
|
||||
Rhino::Context.open do |context|
|
||||
context['Foo'] = Foo
|
||||
context.eval('var foo = new Foo()')
|
||||
context.eval('foo.bar') # get property using reader
|
||||
context.eval('foo.bar = null') # set property using writer
|
||||
context.eval('foo.check_bar()') # called like a function
|
||||
end
|
||||
|
||||
=== Safe by default
|
||||
|
||||
The Ruby Rhino is designed to let you evaluate javascript as safely as possible unless you tell it to do something more
|
||||
|
@ -135,7 +183,7 @@ exposed by default. E.g.
|
|||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2009 Charles Lowell
|
||||
Copyright (c) 2009-2012 Charles Lowell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -9,3 +9,5 @@ end
|
|||
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new
|
||||
|
||||
task :default => :spec
|
||||
|
|
|
@ -46,9 +46,18 @@ module Rhino
|
|||
def eval(javascript)
|
||||
new.eval(javascript)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@default_factory = nil
|
||||
def self.default_factory
|
||||
@@default_factory ||= ContextFactory.new
|
||||
end
|
||||
|
||||
def self.default_factory=(factory)
|
||||
@@default_factory = factory
|
||||
end
|
||||
|
||||
attr_reader :scope
|
||||
|
||||
# Create a new javascript environment for executing javascript and ruby code.
|
||||
|
@ -56,8 +65,9 @@ module Rhino
|
|||
# * <tt>:with</tt> - use this ruby object as the root scope for all javascript that is evaluated
|
||||
# * <tt>:java</tt> - if true, java packages will be accessible from within javascript
|
||||
def initialize(options = {}) #:nodoc:
|
||||
@factory = ContextFactory.new
|
||||
@factory.call do |context|
|
||||
factory = options[:factory] ||
|
||||
(options[:restrictable] ? RestrictableContextFactory.instance : self.class.default_factory)
|
||||
factory.call do |context|
|
||||
@native = context
|
||||
@global = @native.initStandardObjects(nil, options[:sealed] == true)
|
||||
if with = options[:with]
|
||||
|
@ -73,7 +83,12 @@ module Rhino
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns the ContextFactory used while creating this context.
|
||||
def factory
|
||||
@native.getFactory
|
||||
end
|
||||
|
||||
# Read a value from the global scope of this context
|
||||
def [](key)
|
||||
@scope[key]
|
||||
|
@ -117,14 +132,44 @@ module Rhino
|
|||
end
|
||||
end
|
||||
|
||||
# Returns true if this context supports restrictions.
|
||||
def restrictable?
|
||||
@native.is_a?(RestrictableContextFactory::Context)
|
||||
end
|
||||
|
||||
def instruction_limit
|
||||
restrictable? ? @native.instruction_limit : false
|
||||
end
|
||||
|
||||
# Set the maximum number of instructions that this context will execute.
|
||||
# If this instruction limit is exceeded, then a Rhino::RunawayScriptError
|
||||
# will be raised
|
||||
# If this instruction limit is exceeded, then a #Rhino::RunawayScriptError
|
||||
# will be raised.
|
||||
def instruction_limit=(limit)
|
||||
@native.setInstructionObserverThreshold(limit)
|
||||
@factory.instruction_limit = limit
|
||||
if restrictable?
|
||||
@native.instruction_limit = limit
|
||||
else
|
||||
raise "setting an instruction_limit has no effect on this context, use " +
|
||||
"Context.open(:restricted => true) to gain a restrictable instance"
|
||||
end
|
||||
end
|
||||
|
||||
def timeout_limit
|
||||
restrictable? ? @native.timeout_limit : false
|
||||
end
|
||||
|
||||
# Set the duration (in seconds e.g. 1.5) this context is allowed to execute.
|
||||
# After the timeout passes (no matter if any JS has been evaluated) and this
|
||||
# context is still attempted to run code, a #Rhino::ScriptTimeoutError will
|
||||
# be raised.
|
||||
def timeout_limit=(limit)
|
||||
if restrictable?
|
||||
@native.timeout_limit = limit
|
||||
else
|
||||
raise "setting an timeout_limit has no effect on this context, use " +
|
||||
"Context.open(:restricted => true) to gain a restrictable instance"
|
||||
end
|
||||
end
|
||||
|
||||
def optimization_level
|
||||
@native.getOptimizationLevel
|
||||
end
|
||||
|
@ -134,7 +179,7 @@ module Rhino
|
|||
# By using the -1 optimization level, you tell Rhino to run in interpretative mode,
|
||||
# taking a hit to performance but escaping the Java bytecode limit.
|
||||
def optimization_level=(level)
|
||||
if @native.class.isValidOptimizationLevel(level)
|
||||
if JS::Context.isValidOptimizationLevel(level)
|
||||
@native.setOptimizationLevel(level)
|
||||
level
|
||||
else
|
||||
|
@ -157,13 +202,12 @@ module Rhino
|
|||
def version=(version)
|
||||
const = version.to_s.gsub('.', '_').upcase
|
||||
const = "VERSION_#{const}" if const[0, 7] != 'VERSION'
|
||||
js_context = @native.class # Context
|
||||
if js_context.constants.include?(const)
|
||||
const_value = js_context.const_get(const)
|
||||
if JS::Context.constants.find { |c| c.to_s == const }
|
||||
const_value = JS::Context.const_get(const)
|
||||
@native.setLanguageVersion(const_value)
|
||||
const_value
|
||||
else
|
||||
@native.setLanguageVersion(js_context::VERSION_DEFAULT)
|
||||
@native.setLanguageVersion(JS::Context::VERSION_DEFAULT)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
@ -179,11 +223,11 @@ module Rhino
|
|||
private
|
||||
|
||||
def do_open
|
||||
factory.enterContext(@native)
|
||||
begin
|
||||
@factory.enterContext(@native)
|
||||
yield self
|
||||
ensure
|
||||
JS::Context.exit
|
||||
factory.exit
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -216,22 +260,115 @@ module Rhino
|
|||
|
||||
end
|
||||
|
||||
class ContextFactory < JS::ContextFactory # :nodoc:
|
||||
ContextFactory = JS::ContextFactory # :nodoc: backward compatibility
|
||||
|
||||
def observeInstructionCount(cxt, count)
|
||||
raise RunawayScriptError, "script exceeded allowable instruction count" if count > @limit
|
||||
class RestrictableContextFactory < ContextFactory # :nodoc:
|
||||
|
||||
@@instance = nil
|
||||
def self.instance
|
||||
@@instance ||= new
|
||||
end
|
||||
|
||||
# protected Context makeContext()
|
||||
def makeContext
|
||||
Context.new(self)
|
||||
end
|
||||
|
||||
# protected void observeInstructionCount(Context context, int instructionCount)
|
||||
def observeInstructionCount(context, count)
|
||||
context.check!(count) if context.is_a?(Context)
|
||||
end
|
||||
|
||||
# protected Object doTopCall(Callable callable, Context context,
|
||||
# Scriptable scope, Scriptable thisObj, Object[] args)
|
||||
def doTopCall(callable, context, scope, this, args)
|
||||
context.reset! if context.is_a?(Context)
|
||||
super
|
||||
end
|
||||
|
||||
class Context < JS::Context # :nodoc:
|
||||
|
||||
def instruction_limit=(count)
|
||||
@limit = count
|
||||
def initialize(factory)
|
||||
super(factory)
|
||||
reset!
|
||||
end
|
||||
|
||||
attr_reader :instruction_limit
|
||||
|
||||
def instruction_limit=(limit)
|
||||
treshold = getInstructionObserverThreshold
|
||||
if limit && (treshold == 0 || treshold > limit)
|
||||
setInstructionObserverThreshold(limit)
|
||||
end
|
||||
@instruction_limit = limit
|
||||
end
|
||||
|
||||
attr_reader :instruction_count
|
||||
|
||||
TIMEOUT_INSTRUCTION_TRESHOLD = 42
|
||||
|
||||
attr_reader :timeout_limit
|
||||
|
||||
def timeout_limit=(limit) # in seconds
|
||||
treshold = getInstructionObserverThreshold
|
||||
if limit && (treshold == 0 || treshold > TIMEOUT_INSTRUCTION_TRESHOLD)
|
||||
setInstructionObserverThreshold(TIMEOUT_INSTRUCTION_TRESHOLD)
|
||||
end
|
||||
@timeout_limit = limit
|
||||
end
|
||||
|
||||
attr_reader :start_time
|
||||
|
||||
def check!(count = nil)
|
||||
@instruction_count += count if count
|
||||
check_instruction_limit!
|
||||
check_timeout_limit!(count)
|
||||
end
|
||||
|
||||
def check_instruction_limit!
|
||||
if instruction_limit && instruction_count > instruction_limit
|
||||
raise RunawayScriptError, "script exceeded allowable instruction count: #{instruction_limit}"
|
||||
end
|
||||
end
|
||||
|
||||
def check_timeout_limit!(count = nil)
|
||||
if timeout_limit
|
||||
elapsed_time = Time.now.to_f - start_time.to_f
|
||||
if elapsed_time > timeout_limit
|
||||
raise ScriptTimeoutError, "script exceeded timeout: #{timeout_limit} seconds"
|
||||
end
|
||||
# adapt instruction treshold as needed :
|
||||
if count
|
||||
treshold = getInstructionObserverThreshold
|
||||
if elapsed_time * 2 < timeout_limit
|
||||
next_treshold_guess = treshold * 2
|
||||
if instruction_limit && instruction_limit < next_treshold_guess
|
||||
setInstructionObserverThreshold(instruction_limit)
|
||||
else
|
||||
setInstructionObserverThreshold(next_treshold_guess)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reset!
|
||||
@instruction_count = 0
|
||||
@start_time = Time.now
|
||||
self
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContextError < StandardError # :nodoc:
|
||||
end
|
||||
|
||||
class RunawayScriptError < ContextError # :nodoc:
|
||||
end
|
||||
|
||||
class ScriptTimeoutError < ContextError # :nodoc:
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -87,11 +87,11 @@ class Java::OrgMozillaJavascript::ScriptableObject
|
|||
|
||||
# Delegate methods to JS object if possible when called from Ruby.
|
||||
def method_missing(name, *args)
|
||||
s_name = name.to_s
|
||||
if s_name[-1, 1] == '=' && args.size == 1 # writer -> JS put
|
||||
self[ s_name[0...-1] ] = args[0]
|
||||
name_str = name.to_s
|
||||
if name_str[-1, 1] == '=' && args.size == 1 # writer -> JS put
|
||||
self[ name_str[0...-1] ] = args[0]
|
||||
else
|
||||
if property = self[s_name]
|
||||
if property = self[name_str]
|
||||
if property.is_a?(Rhino::JS::Function)
|
||||
begin
|
||||
context = Rhino::JS::Context.enter
|
||||
|
@ -103,7 +103,7 @@ class Java::OrgMozillaJavascript::ScriptableObject
|
|||
end
|
||||
else
|
||||
if args.size > 0
|
||||
raise ArgumentError, "can't #{name}(#{args.join(', ')}) as '#{name}' is a property"
|
||||
raise ArgumentError, "can't call '#{name_str}' with args: #{args.inspect} as it's a property"
|
||||
end
|
||||
Rhino.to_ruby property
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ module Rhino
|
|||
ids = []
|
||||
unwrap.public_methods(false).each do |name|
|
||||
name = name[0...-1] if name[-1, 1] == '=' # 'foo=' ... 'foo'
|
||||
name = name.to_java # java.lang.String
|
||||
name = name.to_s.to_java # java.lang.String
|
||||
ids << name unless ids.include?(name)
|
||||
end
|
||||
super.each { |id| ids.unshift(id) }
|
||||
|
|
|
@ -4,7 +4,7 @@ module Rhino
|
|||
|
||||
def self.has(object, name, scope)
|
||||
if object.respond_to?(name.to_s) ||
|
||||
object.respond_to?("#{name}=") # might have a writer but no reader
|
||||
object.respond_to?(:"#{name}=") # might have a writer but no reader
|
||||
return true
|
||||
end
|
||||
# try [](name) method :
|
||||
|
@ -15,10 +15,12 @@ module Rhino
|
|||
end
|
||||
|
||||
def self.get(object, name, scope)
|
||||
if object.respond_to?(name_s = name.to_s)
|
||||
method = object.method(name_s)
|
||||
name_sym = name.to_s.to_sym
|
||||
if object.respond_to?(name_sym)
|
||||
method = object.method(name_sym)
|
||||
if method.arity == 0 && # check if it is an attr_reader
|
||||
( object.respond_to?("#{name}=") || object.instance_variables.include?("@#{name}") )
|
||||
( object.respond_to?(:"#{name}=") ||
|
||||
object.instance_variables.find { |var| var.to_sym == :"@#{name}" } )
|
||||
begin
|
||||
return Rhino.to_javascript(method.call, scope)
|
||||
rescue => e
|
||||
|
@ -27,7 +29,7 @@ module Rhino
|
|||
else
|
||||
return Function.wrap(method.unbind)
|
||||
end
|
||||
elsif object.respond_to?("#{name}=")
|
||||
elsif object.respond_to?(:"#{name}=")
|
||||
return nil # it does have the property but is non readable
|
||||
end
|
||||
# try [](name) method :
|
||||
|
@ -40,7 +42,7 @@ module Rhino
|
|||
end
|
||||
|
||||
def self.put(object, name, value)
|
||||
if object.respond_to?(set_name = "#{name}=")
|
||||
if object.respond_to?(set_name = :"#{name}=")
|
||||
return object.send(set_name, Rhino.to_ruby(value))
|
||||
end
|
||||
# try []=(name, value) method :
|
||||
|
|
|
@ -65,5 +65,79 @@ describe Rhino::Context do
|
|||
context.version = '1.7'
|
||||
context.version.should == 1.7
|
||||
end
|
||||
|
||||
it "should have a (shared) factory by default" do
|
||||
context1 = Rhino::Context.new
|
||||
context1.factory.should_not be nil
|
||||
context1.factory.should be_a(Rhino::JS::ContextFactory)
|
||||
|
||||
context1.factory.should be Rhino::Context.default_factory
|
||||
|
||||
context2 = Rhino::Context.new
|
||||
context2.factory.should be context1.factory
|
||||
end
|
||||
|
||||
it "allows limiting instruction count" do
|
||||
context = Rhino::Context.new :restrictable => true
|
||||
context.instruction_limit = 100
|
||||
lambda {
|
||||
context.eval %Q{ for (var i = 0; i < 100; i++) Number(i).toString(); }
|
||||
}.should raise_error(Rhino::RunawayScriptError)
|
||||
|
||||
context.instruction_limit = nil
|
||||
lambda {
|
||||
context.eval %Q{ for (var i = 0; i < 100; i++) Number(i).toString(); }
|
||||
}.should_not raise_error(Rhino::RunawayScriptError)
|
||||
end
|
||||
|
||||
it "allows a timeout limit per context" do
|
||||
context1 = Rhino::Context.new :restrictable => true, :java => true
|
||||
context1.timeout_limit = 0.3
|
||||
|
||||
context2 = Rhino::Context.new :restrictable => true, :java => true
|
||||
context2.timeout_limit = 0.3
|
||||
|
||||
lambda {
|
||||
context2.eval %Q{
|
||||
var notDone = true;
|
||||
(function foo() {
|
||||
if (notDone) {
|
||||
notDone = false;
|
||||
java.lang.Thread.sleep(300);
|
||||
foo();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}.should raise_error(Rhino::ScriptTimeoutError)
|
||||
|
||||
lambda {
|
||||
context1.eval %Q{
|
||||
var notDone = true;
|
||||
(function foo {
|
||||
if (notDone) {
|
||||
notDone = false;
|
||||
java.lang.Thread.sleep(100);
|
||||
foo();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}.should_not raise_error(Rhino::RunawayScriptError)
|
||||
end
|
||||
|
||||
it "allows instruction and timeout limits at the same time" do
|
||||
context = Rhino::Context.new :restrictable => true, :java => true
|
||||
context.timeout_limit = 0.5
|
||||
context.instruction_limit = 10000
|
||||
lambda {
|
||||
context.eval %Q{ for (var i = 0; i < 100; i++) { java.lang.Thread.sleep(100); } }
|
||||
}.should raise_error(Rhino::ScriptTimeoutError)
|
||||
|
||||
context = Rhino::Context.new :restrictable => true, :java => true
|
||||
context.timeout_limit = 0.5
|
||||
context.instruction_limit = 1000
|
||||
lambda {
|
||||
context.eval %Q{ for (var i = 0; i < 100; i++) { java.lang.Thread.sleep(10); } }
|
||||
}.should raise_error(Rhino::RunawayScriptError)
|
||||
end
|
||||
|
||||
end
|
|
@ -132,12 +132,12 @@ describe Rhino::To do
|
|||
it "converts procs and methods into native functions" do
|
||||
Rhino.to_javascript(lambda {|lhs,rhs| lhs * rhs}).tap do |f|
|
||||
f.should be_kind_of(Rhino::JS::Function)
|
||||
f.call(nil, nil, nil, [7,6]).should be(42)
|
||||
f.call(nil, nil, nil, [7, 6].to_java).should be(42)
|
||||
end
|
||||
|
||||
Rhino.to_javascript("foo,bar,baz".method(:split)).tap do |m|
|
||||
m.should be_kind_of(Rhino::JS::Function)
|
||||
Rhino.to_ruby(m.call(nil, nil, nil, ',')).should == ['foo', 'bar', 'baz']
|
||||
Rhino.to_javascript("foo,bar,baz".method(:split)).tap do |f|
|
||||
f.should be_kind_of(Rhino::JS::Function)
|
||||
Rhino.to_ruby(f.call(nil, nil, nil, [','].to_java)).should == ['foo', 'bar', 'baz']
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue