Skip to content

Commit

Permalink
Merge pull request #14 from kares/master
Browse files Browse the repository at this point in the history
context timeout restriction and 1.9 compatibility
  • Loading branch information
cowboyd committed Apr 9, 2012
2 parents 9b94864 + 3f68bb0 commit 0dd3f7e
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
@@ -0,0 +1,6 @@
rvm:
- jruby-18mode
- jruby-19mode
branches:
only:
- master
62 changes: 55 additions & 7 deletions README.rdoc
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Rakefile
Expand Up @@ -9,3 +9,5 @@ end

require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new

task :default => :spec
181 changes: 159 additions & 22 deletions lib/rhino/context.rb
Expand Up @@ -46,18 +46,28 @@ def open(options = {}, &block)
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.
# * <tt>:sealed</tt> - if this is true, then the standard objects such as Object, Function, Array will not be able to be modified
# * <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]
Expand All @@ -73,7 +83,12 @@ def initialize(options = {}) #:nodoc:
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]
Expand Down Expand Up @@ -117,14 +132,44 @@ def load(filename)
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
Expand All @@ -134,7 +179,7 @@ def optimization_level
# 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
Expand All @@ -157,13 +202,12 @@ def version
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
Expand All @@ -179,11 +223,11 @@ def open(&block)
private

def do_open
factory.enterContext(@native)
begin
@factory.enterContext(@native)
yield self
ensure
JS::Context.exit
factory.exit
end
end

Expand Down Expand Up @@ -216,22 +260,115 @@ def read(buffer, offset, length)

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 initialize(factory)
super(factory)
reset!
end

def instruction_limit=(count)
@limit = count
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
10 changes: 5 additions & 5 deletions lib/rhino/rhino_ext.rb
Expand Up @@ -87,11 +87,11 @@ def to_json(*args)

# 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
Expand All @@ -103,7 +103,7 @@ def method_missing(name, *args)
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
Expand Down

0 comments on commit 0dd3f7e

Please sign in to comment.