セーフレベルの高い環境から、特定のオブジェクトのメソッド呼び出しを別のセーフレベルで実行するユーティリティ
ネタがないので、昔書いたクラスでも発掘するかな。ということで、前に書いたセーフレベルの高い環境から、特定のオブジェクトのメソッド呼び出しを低いセーフレベルで実行するためのユーティリティです。仕組みは以下のとおり。
- 低いセーフレベルで動作するスレッドをあらかじめ起動させておき、
- Permitter#proxyで、指定されたオブジェクトのメソッドの実行を起動しておいたスレッドで行うように改変。
- メソッド実行を低いセーフレベルで動作するスレッドで行うことで、セーフレベルの高い環境から普通は実行できない処理を含むメソッドを呼び出せるようにします。
サンプル
#任意のセーフレベルでブロックを実行する def safe( level=4 ) return Thread.fork(level) {|l| $SAFE = l yield }.value end class TestClass def test puts "test" # IOを行うのでセーフレベルの高い環境で実行するとエラーになる。 end end #TestClassのtestメソッドはセーフレベル4で実行されるとエラーになる。 test = TestClass.new begin safe(4) { test.test } raise "ここにはこないはず" rescue SecurityError end #Permitterでプロキシをかける。 #プロキシをかけたオブジェクトのメソッドは、セーフレベルの高い環境でも実行を許可できる。 begin p = Permitter.new permitted = p.proxy( test, [/test/] ) safe(4) { permitted.test # 実行可能 } #クラスの改変は不可なのでセーフレベルの高い環境でのプロキシの適用は不可。 test2 = TestClass.new safe(4) { begin permitted2 = p.proxy( test2, [/test/] ) raise "ここにはこないはず" rescue SecurityError end } ensure p.close if p end
実装
require 'thread' require 'set' #==任意のオブジェクトのAPIをセーフレベルの高い環境から呼び出せるようにする。 class Permitter # コンストラクタ #pool_size:: スレッドプールのサイズ #level:: メソッド実行時のセーフレベル def initialize( pool_size=5, level=2 ) @alive = true @alive_mutex = Mutex.new @q = Queue.new @ts = [] pool_size.times { @ts << Thread.fork { $SAFE = level while( @alive_mutex.synchronize { @alive } ) req = @q.pop req.exec(self) if req end } } end # リクエストを追加する。 def <<(request) @q.push request end # インスタンスを破棄する。不要になった場合に必ず呼び出すこと。 def close @alive_mutex.synchronize { @alive = false } @ts.length.times { @q.push nil } @ts.each {|t| t.join } end # 指定したインスタンスのAPIをセーフレベルの高い環境から呼び出せるようにする。 #object:: APIの呼び出しを許可するオブジェクト #allows:: 許可するAPIを示す正規表現の配列 #proxy_result:: 戻り値もプロキシを設定するAPIを示す正規表現の配列 def proxy( object, allows=[], proxy_result=[]) clazz = class << object include Permitted self end object.permitter = self object.permitter_allows = allows object.permitter_proxy_result = proxy_result object.methods.each {|name| next unless allows.find {|a| name.to_s =~ a } # もともとのメソッドは、別名でキープしておく old = name.to_s + "_without_permittion" # 2重のアスペクト適用を防ぐ。 next if clazz.method_defined? old.to_sym clazz.__send__(:alias_method, old.to_sym, name.to_sym ) # インターセプタ適用対象のメソッドを上書きで定義。 clazz.__send__(:alias_method, name.to_sym, :permitted_request ) } object end # メソッド呼び出しリクエスト class Request def initialize( receiver, name, args, block=nil, allows=[], proxy_result=[] ) @name = name @args = args @block = block @receiver = receiver @mutex = Mutex.new @cv = ConditionVariable.new @finished = false @value = nil @error = nil @allows = allows @proxy_result = proxy_result end # リクエストの完了を待ち、結果を返す。 def wait @mutex.synchronize{ @cv.wait( @mutex ) until @finished } raise @error if @error @value end # リクエストを実行する def exec( permitter ) begin m = @name + "_without_permittion" @value = @block ? @receiver.send( m, *@args, &@block ) : @receiver.send( m, *@args ) if @proxy_result.find {|a| @name.to_s =~ a } @value = permitter.proxy( @value, @allows, @proxy_result ) end rescue Exception @error = $! ensure @mutex.synchronize{ @finished = true @cv.signal } end end end end module Permitted def permitted_request( *args, &block ) r = Permitter::Request.new( self, _function_name, args, block, @permitter_allows, @permitter_proxy_result ) @permitter << r return r.wait end def _function_name if /^(.+?):(\d+)(?::in `(.*)')?/ =~ caller.first return $3 end end attr :permitter, true attr :permitter_allows, true attr :permitter_proxy_result, true end
セキュリティホールになってないか若干不安なんですが。大丈夫かな。