Contrib/Evland was removed due to security issues and unsupported code. The code can be found for reference at https://github.com/Griatch/evlang.
This commit is contained in:
parent
ef0a154a61
commit
b7b68afe20
6 changed files with 0 additions and 1372 deletions
|
|
@ -1,114 +0,0 @@
|
||||||
|
|
||||||
EVLANG
|
|
||||||
|
|
||||||
EXPERIMENTAL IMPLEMENTATION
|
|
||||||
|
|
||||||
Evennia contribution - Griatch 2012
|
|
||||||
|
|
||||||
"Evlang" is a heavily restricted version of Python intended to be used
|
|
||||||
by regular players to code simple functionality on supporting objects.
|
|
||||||
It's referred to as "evlang" or "evlang scripts" in order to
|
|
||||||
differentiate from Evennia's normal (and unrelated) "Scripts".
|
|
||||||
|
|
||||||
WARNING:
|
|
||||||
Restricted python execution is a tricky art, and this module -is-
|
|
||||||
partly based on blacklisting techniques, which might be vulnerable to
|
|
||||||
new venues of attack opening up in the future (or existing ones we've
|
|
||||||
missed). Whereas I/we know of no obvious exploits to this, it is no
|
|
||||||
guarantee. If you are paranoid about security, consider also using
|
|
||||||
secondary defences on the OS level such as a jail and highly
|
|
||||||
restricted execution abilities for the twisted process. So in short,
|
|
||||||
this should work fine, but use it at your own risk. You have been
|
|
||||||
warned.
|
|
||||||
|
|
||||||
An Evennia server with Evlang will, once set up, minimally consist of
|
|
||||||
the following components:
|
|
||||||
|
|
||||||
- The evlang parser (bottom of evlang.py). This combines
|
|
||||||
regular removal of dangerous modules/builtins with AST-traversal.
|
|
||||||
it implements a limited_exec() function.
|
|
||||||
- The Evlang handler (top of evlang.py). This handler is the Evennia
|
|
||||||
entry point. It should be added to objects that should support
|
|
||||||
evlang-scripting.
|
|
||||||
- A custom object typeclass. This must set up the Evlang handler
|
|
||||||
and store a few critical Attributes on itself for book-keeping.
|
|
||||||
The object will probably also overload some of its hooks to
|
|
||||||
call the correct evlang script at the proper time
|
|
||||||
- Command(s) for adding code to supporting objects
|
|
||||||
- Optional expanded "safe" methods/objects to include in the
|
|
||||||
execution environment. These are defined in settings (see
|
|
||||||
header of evlang.py for more info).
|
|
||||||
|
|
||||||
You can set this up easily to try things out by using the included
|
|
||||||
examples:
|
|
||||||
|
|
||||||
Quick Example Install
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
This is a quick test-setup using the example objects and commands.
|
|
||||||
|
|
||||||
1) If you haven't already, make sure you are able to overload the
|
|
||||||
default cmdset: Copy game/gamesrc/commands/examples/cmdset.py up
|
|
||||||
one level, then change settings.CMDSET_DEFAULT to point to
|
|
||||||
DefaultCmdSet in your newly copied module. Restart the server and
|
|
||||||
check so the default commands still work.
|
|
||||||
2) Import and add
|
|
||||||
contrib.evlang.command.CmdCode
|
|
||||||
and
|
|
||||||
contrib.evlang.examples.CmdCraftScriptable
|
|
||||||
to your default command set. Reload server.
|
|
||||||
|
|
||||||
That's it, really. You should now have two new commands available,
|
|
||||||
@craftscriptable and @code. The first one is a simple "crafting-like"
|
|
||||||
command that will create an object of type
|
|
||||||
contrib.evlang.examples.CraftedScriptableObject while setting it up
|
|
||||||
with some basic scripting slots.
|
|
||||||
|
|
||||||
Try it now:
|
|
||||||
|
|
||||||
@craftscriptable crate
|
|
||||||
|
|
||||||
You create a simple "crate" object in your current location. You can
|
|
||||||
use @code to see which "code types" it will accept.
|
|
||||||
|
|
||||||
@code crate
|
|
||||||
|
|
||||||
You should see a list with "drop", "get" and "look", each without
|
|
||||||
anything assigned to them. If you look at how CraftedScriptableObject
|
|
||||||
is defined you will find that these "command types" (you can think of
|
|
||||||
them as slots where custom code can be put) are tied to the at_get,
|
|
||||||
at_drop and at_desc hooks respecively - this means Evlang scripts put
|
|
||||||
in the respective slots will ttrigger at the appropriate time.
|
|
||||||
|
|
||||||
There are a few "safe" objects made available out of the box.
|
|
||||||
|
|
||||||
self - reference to object the Evlang handler is defined on
|
|
||||||
here - shortcut for self.location
|
|
||||||
caller - reference back to the one triggering the script
|
|
||||||
scripter - reference to the one creating the script (set by @code)
|
|
||||||
|
|
||||||
There is also the 'evl' object that defines "safe" methods to use:
|
|
||||||
|
|
||||||
evl.msg(string, obj=None) # default is the send to caller
|
|
||||||
evl.msg_contents(string, obj=None) # default is to send to all except caller
|
|
||||||
evl.msg_home(string, obj=None) # default is to send to self.location
|
|
||||||
delay(delay, function, *args, **kwargs)
|
|
||||||
attr(obj, attrname=None, attrvalue=None, delete=False) # lock-checking attribute accesser
|
|
||||||
list() # display all available methods on evl, with docstrings (including your custom additions)
|
|
||||||
|
|
||||||
These all return True after successful execution, which makes
|
|
||||||
especially the msg* functions easier to use in a conditional. Let's
|
|
||||||
try it.
|
|
||||||
|
|
||||||
@code crate/look = caller.key=='Superman' and evl.msg("Your gaze burns a small hole.") or evl.msg("Looks robust!")
|
|
||||||
|
|
||||||
Now look at the crate. :)
|
|
||||||
|
|
||||||
You can (in evlang) use evl.list() to get a list of all methods
|
|
||||||
currently stored on the evl object. For testing, let's use the same
|
|
||||||
look slot on the crate again. But this time we'll use the /debug mode
|
|
||||||
of @code, which means the script will be auto-run immediately and we
|
|
||||||
don't have to look at the create to get a result when developing.
|
|
||||||
|
|
||||||
@code/debug crate/look = evl.msg(evl.list())
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
Evlang usage examples
|
|
||||||
Commands for use with evlang
|
|
||||||
|
|
||||||
Evennia contribution - Griatch 2012
|
|
||||||
|
|
||||||
The @code command allows to add scripted evlang code to
|
|
||||||
a ScriptableObject. It will handle access checks.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ev import utils
|
|
||||||
from ev import default_cmds
|
|
||||||
from src.utils import prettytable
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Evlang-related commands
|
|
||||||
#
|
|
||||||
# Easiest is to add this command to the default cmdset.
|
|
||||||
# Alternatively one could imagine storing it directly only
|
|
||||||
# on scriptable objects.
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
class CmdCode(default_cmds.MuxCommand):
|
|
||||||
"""
|
|
||||||
add custom code to a scriptable object
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
@code[/switch] <obj>[/<type> [= <codestring> ]]
|
|
||||||
|
|
||||||
Switch:
|
|
||||||
|
|
||||||
delete - clear code of given type from the object.
|
|
||||||
debug - immediately run the given code after adding it.
|
|
||||||
|
|
||||||
This will add custom scripting to an object
|
|
||||||
which allows such modification.
|
|
||||||
|
|
||||||
<type> must be one of the script types allowed
|
|
||||||
on the object. Only supplying the command will
|
|
||||||
return a list of script types possible to add
|
|
||||||
custom scripts to.
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "@code"
|
|
||||||
locks = "cmd:perm(Builders)"
|
|
||||||
help_category = "Building"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"implements the functionality."
|
|
||||||
caller = self.caller
|
|
||||||
|
|
||||||
if not self.args:
|
|
||||||
caller.msg("Usage: @code <obj>[/<type> [= <codestring>]]")
|
|
||||||
return
|
|
||||||
codetype = None
|
|
||||||
objname = self.lhs
|
|
||||||
if '/' in self.lhs:
|
|
||||||
objname, codetype = [part.strip() for part in self.lhs.rsplit("/", 1)]
|
|
||||||
|
|
||||||
obj = self.caller.search(objname)
|
|
||||||
if not obj:
|
|
||||||
return
|
|
||||||
|
|
||||||
# get the dicts from db storage for easy referencing
|
|
||||||
evlang_scripts = obj.db.evlang_scripts
|
|
||||||
evlang_locks = obj.db.evlang_locks
|
|
||||||
if not (evlang_scripts != None and evlang_locks and obj.ndb.evlang):
|
|
||||||
caller.msg("Object %s can not be scripted." % obj.key)
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'delete' in self.switches:
|
|
||||||
# clearing a code snippet
|
|
||||||
if not codetype:
|
|
||||||
caller.msg("You must specify a code type.")
|
|
||||||
return
|
|
||||||
if not codetype in evlang_scripts:
|
|
||||||
caller.msg("Code type '%s' not found on %s." % (codetype, obj.key))
|
|
||||||
return
|
|
||||||
# this will also update the database
|
|
||||||
obj.ndb.evlang.delete(codetype)
|
|
||||||
caller.msg("Code for type '%s' cleared on %s." % (codetype, obj.key))
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.rhs:
|
|
||||||
if codetype:
|
|
||||||
scripts = [(name, tup[1], utils.crop(tup[0]))
|
|
||||||
for name, tup in evlang_scripts.items() if name==codetype]
|
|
||||||
scripts.extend([(name, "--", "--") for name in evlang_locks
|
|
||||||
if name not in evlang_scripts if name==codetype])
|
|
||||||
else:
|
|
||||||
# no type specified. List all scripts/slots on object
|
|
||||||
print evlang_scripts
|
|
||||||
scripts = [(name, tup[1], utils.crop(tup[0]))
|
|
||||||
for name, tup in evlang_scripts.items()]
|
|
||||||
scripts.extend([(name, "--", "--") for name in evlang_locks
|
|
||||||
if name not in evlang_scripts])
|
|
||||||
scripts = sorted(scripts, key=lambda p: p[0])
|
|
||||||
|
|
||||||
table = prettytable.PrettyTable(["{wtype", "{wcreator", "{wcode"])
|
|
||||||
for tup in scripts:
|
|
||||||
table.add_row([tup[0], tup[1], tup[2]])
|
|
||||||
string = "{wEvlang scripts on %s:{n\n%s" % (obj.key, table)
|
|
||||||
caller.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# we have rhs
|
|
||||||
codestring = self.rhs
|
|
||||||
if not codetype in evlang_locks:
|
|
||||||
caller.msg("Code type '%s' cannot be coded on %s." % (codetype, obj.key))
|
|
||||||
return
|
|
||||||
# check access with the locktype "code"
|
|
||||||
if not obj.ndb.evlang.lockhandler.check(caller, "code"):
|
|
||||||
caller.msg("You are not permitted to add code of type %s." % codetype)
|
|
||||||
return
|
|
||||||
# we have code access to this type.
|
|
||||||
oldcode = None
|
|
||||||
if codetype in evlang_scripts:
|
|
||||||
oldcode = str(evlang_scripts[codetype][0])
|
|
||||||
# this updates the database right away too
|
|
||||||
obj.ndb.evlang.add(codetype, codestring, scripter=caller)
|
|
||||||
if oldcode:
|
|
||||||
caller.msg("{wReplaced{n\n %s\n{wWith{n\n %s" % (oldcode, codestring))
|
|
||||||
else:
|
|
||||||
caller.msg("Code added in '%s':\n %s" % (codetype, codestring))
|
|
||||||
if "debug" in self.switches:
|
|
||||||
# debug mode
|
|
||||||
caller.msg("{wDebug: running script (look out for errors below) ...{n\n" + "-"*68)
|
|
||||||
obj.ndb.evlang.run_by_name(codetype, caller, quiet=False)
|
|
||||||
|
|
@ -1,955 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
EVLANG
|
|
||||||
|
|
||||||
A mini-language for online coding of Evennia
|
|
||||||
|
|
||||||
Evennia contribution - Griatch 2012
|
|
||||||
|
|
||||||
WARNING:
|
|
||||||
Restricted python execution is a tricky art, and this module -is-
|
|
||||||
partly based on blacklisting techniques, which might be vulnerable to
|
|
||||||
new venues of attack opening up in the future (or existing ones we've
|
|
||||||
missed). Whereas I/we know of no obvious exploits to this, it is no
|
|
||||||
guarantee. If you are paranoid about security, consider also using
|
|
||||||
secondary defences on the OS level such as a jail and highly
|
|
||||||
restricted execution abilities for the twisted process. So in short,
|
|
||||||
this should work fine, but use it at your own risk. You have been
|
|
||||||
warned.
|
|
||||||
|
|
||||||
This module offers a highly restricted execution environment for users
|
|
||||||
to script objects in an almost-Python language. It's not really a true
|
|
||||||
sandbox but based on a very stunted version of Python. This not only
|
|
||||||
restricts obvious things like import statements and other builins, but
|
|
||||||
also pre-parses the AST tree to completely kill whole families of
|
|
||||||
functionality. The result is a subset of Python that -should- keep an
|
|
||||||
untrusted, malicious user from doing bad things to the server.
|
|
||||||
|
|
||||||
An important limitation with this this implementation is a lack of a
|
|
||||||
timeout check - inside Twisted (and in Python in general) it's very
|
|
||||||
hard to safely kill a thread with arbitrary code once it's running. So
|
|
||||||
instead we restrict the most common DOS-attack vectors, such as while
|
|
||||||
loops, huge power-law assignments as well as function definitions. A
|
|
||||||
better way would probably be to spawn the runner into a separate
|
|
||||||
process but that stunts much of the work a user might want to do with
|
|
||||||
objects (since the current in-memory state of an object has potential
|
|
||||||
importance in Evennia). If you want to try the subprocess route, you
|
|
||||||
might want to look into hacking the Evlang handler (below) onto code
|
|
||||||
from the pysandbox project (https://github.com/haypo/pysandbox). Note
|
|
||||||
however, that one would probably need to rewrite that to use Twisted's
|
|
||||||
non-blocking subprocess mechanisms instead.
|
|
||||||
|
|
||||||
|
|
||||||
The module holds the "Evlang" handler, which is intended to be the
|
|
||||||
entry point for adding scripting support anywhere in Evennia.
|
|
||||||
|
|
||||||
By default the execution environment makes the following objects
|
|
||||||
available (some or all of these may be None depending on how the
|
|
||||||
code was launched):
|
|
||||||
caller - a reference to the object triggering the code
|
|
||||||
scripter - the original creator of the code
|
|
||||||
self - the object on which the code is defined
|
|
||||||
here - shortcut to self.location, if applicable
|
|
||||||
|
|
||||||
There is finally a variable "evl" which is a holder object for safe
|
|
||||||
functions to execute. This object is initiated with the objects above,
|
|
||||||
to make sure the user does not try to forge the input arguments. See
|
|
||||||
below the default safe methods defined on it.
|
|
||||||
|
|
||||||
You can add new safe symbols to the execution context by adding
|
|
||||||
EVLANG_SAFE_CONTEXT to your settings file. This should be a dictionary
|
|
||||||
with {"name":object} pairs.
|
|
||||||
|
|
||||||
You can also add new safe methods to the evl object. You add them as a
|
|
||||||
dictionary on the same form to settings.EVLANG_SAFE_METHODS. Remember
|
|
||||||
that such meethods must be defined properly to be a class method
|
|
||||||
(notably "self" must be be the first argument on the argument list).
|
|
||||||
|
|
||||||
You can finally define settings.EVLANG_UNALLOWED_SYMBOLS as a list of
|
|
||||||
python symbol names you specifically want to lock. This will lock both
|
|
||||||
functions of that name as well as trying to access attributes on
|
|
||||||
objects with that name (note that these "attributes" have nothing to
|
|
||||||
do with Evennia's in-database "Attribute" system!).
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys, os, time
|
|
||||||
import __builtin__
|
|
||||||
import inspect, ast, _ast
|
|
||||||
from twisted.internet import reactor, threads, task
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
# set up django, if necessary
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
||||||
from game import settings
|
|
||||||
try:
|
|
||||||
from django.conf import settings as settings2
|
|
||||||
settings2.configure()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
del settings2
|
|
||||||
|
|
||||||
_LOGGER = None
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Evennia-specific blocks
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
# specifically forbidden symbols
|
|
||||||
_EV_UNALLOWED_SYMBOLS = ["attr", "attributes", "delete"]
|
|
||||||
try:
|
|
||||||
_EV_UNALLOWED_SYMBOLS.expand(settings.EVLANG_UNALLOWED_SYMBOLS)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# safe methods (including self in args) to make available on
|
|
||||||
# the evl object
|
|
||||||
_EV_SAFE_METHODS = {}
|
|
||||||
try:
|
|
||||||
_EV_SAFE_METHODS.update(settings.EVLANG_SAFE_METHODS)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# symbols to make available directly in code
|
|
||||||
_EV_SAFE_CONTEXT = {"testvar": "This is a safe var!"}
|
|
||||||
try:
|
|
||||||
_EV_SAFE_CONTEXT.update(settings.EVLANG_SAFE_CONTEXT)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Holder object for "safe" function access
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
class Evl(object):
|
|
||||||
"""
|
|
||||||
This is a wrapper object for storing safe functions
|
|
||||||
in a secure way, while offering a few properties for
|
|
||||||
them to access. This will be made available as the
|
|
||||||
"evl" property in code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj=None, caller=None, scripter=None, **kwargs):
|
|
||||||
"Populate the object with safe properties"
|
|
||||||
self.obj = obj
|
|
||||||
self.caller = caller
|
|
||||||
self.scripter = scripter
|
|
||||||
self.locatiton = None
|
|
||||||
if obj and hasattr(obj, "location"):
|
|
||||||
self.location = obj.location
|
|
||||||
for key, val in _EV_SAFE_METHODS.items():
|
|
||||||
setattr(self.__class__, name, val)
|
|
||||||
for key, val in kwargs.items():
|
|
||||||
setattr(self.__class__, name, val)
|
|
||||||
|
|
||||||
def list(self):
|
|
||||||
"""
|
|
||||||
list()
|
|
||||||
|
|
||||||
returns a string listing all methods on the evl object, including doc strings."
|
|
||||||
"""
|
|
||||||
# must do it this way since __dict__ is restricted
|
|
||||||
members = [mtup for mtup in inspect.getmembers(Evl, predicate=inspect.ismethod)
|
|
||||||
if not mtup[0].startswith("_")]
|
|
||||||
string = "\n".join(["{w%s{n\n %s" % (mtup[0], mtup[1].func_doc.strip())
|
|
||||||
for mtup in members])
|
|
||||||
return string
|
|
||||||
|
|
||||||
def msg(self, string, obj=None):
|
|
||||||
"""
|
|
||||||
msg(string, obj=None)
|
|
||||||
|
|
||||||
Sends message to obj or to caller if obj is not defined..
|
|
||||||
"""
|
|
||||||
if not obj:
|
|
||||||
obj = self.caller
|
|
||||||
obj.msg(string)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def msg_contents(self, string, obj=None):
|
|
||||||
"""
|
|
||||||
msg_contents(string, obj=None):
|
|
||||||
|
|
||||||
Sends message to the contents of obj, or to content of self if obj is not defined.
|
|
||||||
"""
|
|
||||||
if not obj:
|
|
||||||
obj = self.obj
|
|
||||||
obj.msg_contents(string, exclude=[obj])
|
|
||||||
return True
|
|
||||||
|
|
||||||
def msg_here(self, string, obj=None):
|
|
||||||
"""
|
|
||||||
msg_here(string, obj=None)
|
|
||||||
|
|
||||||
Sends to contents of obj.location, or to self.location if obj is not defined.
|
|
||||||
"""
|
|
||||||
if obj and hasattr(obj, "location"):
|
|
||||||
here = obj.location
|
|
||||||
else:
|
|
||||||
here = self.location
|
|
||||||
if here:
|
|
||||||
here.msg_contents(string)
|
|
||||||
|
|
||||||
def delay(self, seconds, function, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
delay(seconds, function, *args, **kwargs):
|
|
||||||
|
|
||||||
Delay execution of function(*args, **kwargs) for up to 120 seconds.
|
|
||||||
|
|
||||||
Error messages are relayed to caller unless a specific keyword
|
|
||||||
'errobj' is supplied pointing to another object to receiver errors.
|
|
||||||
"""
|
|
||||||
# handle the special error-reporting object
|
|
||||||
errobj = self.caller
|
|
||||||
if "errobj" in kwargs:
|
|
||||||
errobj = kwargs["errobj"]
|
|
||||||
del kwargs["errobj"]
|
|
||||||
# set up some callbacks for delayed execution
|
|
||||||
|
|
||||||
def errback(f, errobj):
|
|
||||||
"error callback"
|
|
||||||
if errobj:
|
|
||||||
try:
|
|
||||||
f = f.getErrorMessage()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
errobj.msg("EVLANG delay error: " + str(f))
|
|
||||||
|
|
||||||
def runfunc(func, *args, **kwargs):
|
|
||||||
"threaded callback"
|
|
||||||
threads.deferToThread(func, *args, **kwargs).addErrback(errback, errobj)
|
|
||||||
# get things going
|
|
||||||
if seconds <= 120:
|
|
||||||
task.deferLater(reactor, seconds, runfunc, function, *args, **kwargs).addErrback(errback, errobj)
|
|
||||||
else:
|
|
||||||
raise EvlangError("delay() can only delay for a maximum of 120 seconds (got %ss)." % seconds)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def attr(self, obj, attrname=None, value=None, delete=False):
|
|
||||||
"""
|
|
||||||
attr(obj, attrname=None, value=None, delete=False)
|
|
||||||
|
|
||||||
Access and edit database Attributes on obj. if only obj
|
|
||||||
is given, return list of Attributes on obj. If attrname
|
|
||||||
is given, return that Attribute's value only. If also
|
|
||||||
value is given, set the attribute to that value. The
|
|
||||||
delete flag will delete the given attrname from the object.
|
|
||||||
|
|
||||||
Access is checked for all operations. The method will return
|
|
||||||
the attribute value or True if the operation was a success,
|
|
||||||
None otherwise.
|
|
||||||
"""
|
|
||||||
print obj, hasattr(obj, "secure_attr")
|
|
||||||
if hasattr(obj, "secure_attr"):
|
|
||||||
return obj.secure_attr(self.caller, attrname, value, delete=False,
|
|
||||||
default_access_read=True, default_access_edit=False,
|
|
||||||
default_access_create=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Evlang class handler
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
class EvlangError(Exception):
|
|
||||||
"Error for evlang handler"
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Evlang(object):
|
|
||||||
"""
|
|
||||||
This is a handler for launching limited execution Python scripts.
|
|
||||||
|
|
||||||
Normally this handler is stored on an object and will then give
|
|
||||||
access to basic operations on the object. It can however also be
|
|
||||||
run stand-alone.
|
|
||||||
|
|
||||||
If running on an object, it should normally be initiated in the
|
|
||||||
object's at_server_start() method and assigned to a property
|
|
||||||
"evlang" (or similar) for easy access. It will then use the object
|
|
||||||
for storing a dictionary of available evlang scripts (default name
|
|
||||||
of this attribute is "evlang_scripts").
|
|
||||||
|
|
||||||
Note: This handler knows nothing about access control. To get that
|
|
||||||
one needs to append a LockHandler as "lockhandler" at creation
|
|
||||||
time, as well as arrange for commands to do access checks of
|
|
||||||
suitable type. Once methods on this handler are called, access is
|
|
||||||
assumed to be granted.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, obj=None, scripts=None, storage_attr="evlang_scripts",
|
|
||||||
safe_context=None, safe_timeout=2):
|
|
||||||
"""
|
|
||||||
Setup of the Evlang handler.
|
|
||||||
|
|
||||||
Input:
|
|
||||||
obj - a reference to the object this handler is defined on. If not
|
|
||||||
set, handler will operate stand-alone.
|
|
||||||
scripts = dictionary {scriptname, (codestring, callerobj), ...}
|
|
||||||
where callerobj can be Noneevlang_storage_attr - if obj
|
|
||||||
is given, will look for a dictionary
|
|
||||||
{scriptname, (codestring, callerobj)...}
|
|
||||||
stored in this given attribute name on that object.
|
|
||||||
safe_funcs - dictionary of {funcname:funcobj, ...} to make available
|
|
||||||
for the execution environment
|
|
||||||
safe_timeout - the time we let a script run. If it exceeds this
|
|
||||||
time, it will be blocked from running again.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.obj = obj
|
|
||||||
self.evlang_scripts = {}
|
|
||||||
self.safe_timeout = safe_timeout
|
|
||||||
self.evlang_storage_attr = storage_attr
|
|
||||||
if scripts:
|
|
||||||
self.evlang_scripts.update(scripts)
|
|
||||||
if self.obj:
|
|
||||||
self.evlang_scripts.update(obj.attributes.get(storage_attr))
|
|
||||||
self.safe_context = _EV_SAFE_CONTEXT # set by default + settings
|
|
||||||
if safe_context:
|
|
||||||
self.safe_context.update(safe_context)
|
|
||||||
self.timedout_codestrings = []
|
|
||||||
|
|
||||||
def msg(self, string, scripter=None, caller=None):
|
|
||||||
"""
|
|
||||||
Try to send string to a receiver. Returns False
|
|
||||||
if no receiver was found.
|
|
||||||
"""
|
|
||||||
if scripter:
|
|
||||||
scripter.msg(string)
|
|
||||||
elif caller:
|
|
||||||
caller.msg(string)
|
|
||||||
elif self.obj:
|
|
||||||
self.obj.msg(string)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def start_timer(self, timeout, codestring, caller, scripter):
|
|
||||||
"""
|
|
||||||
Start a timer to check how long an execution has lasted.
|
|
||||||
Returns a deferred, which should be cancelled when the
|
|
||||||
code does finish.
|
|
||||||
"""
|
|
||||||
def alarm(codestring):
|
|
||||||
"store the code of too-long-running scripts"
|
|
||||||
global _LOGGER
|
|
||||||
if not _LOGGER:
|
|
||||||
from src.utils import logger as _LOGGER
|
|
||||||
self.timedout_codestrings.append(codestring)
|
|
||||||
err = "Evlang code '%s' exceeded allowed execution time (>%ss)." % (codestring, timeout)
|
|
||||||
_LOGGER.log_errmsg("EVLANG time exceeded: caller: %s, scripter: %s, code: %s" % (caller, scripter, codestring))
|
|
||||||
if not self.msg(err, scripter, caller):
|
|
||||||
raise EvlangError(err)
|
|
||||||
|
|
||||||
def errback(f):
|
|
||||||
"We need an empty errback, to catch the traceback of defer.cancel()"
|
|
||||||
pass
|
|
||||||
return task.deferLater(reactor, timeout, alarm, codestring).addErrback(errback)
|
|
||||||
|
|
||||||
def stop_timer(self, _, deferred):
|
|
||||||
"""Callback for stopping a previously started timer.
|
|
||||||
Cancels the given deferred.
|
|
||||||
"""
|
|
||||||
deferred.cancel()
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def run(self, codestring, caller=None, scripter=None):
|
|
||||||
"""
|
|
||||||
run a given code string.
|
|
||||||
|
|
||||||
codestring - the actual code to execute.
|
|
||||||
scripter - the creator of the script. Preferentially sees error messages
|
|
||||||
caller - the object triggering the script - sees error messages if
|
|
||||||
no scripter is given
|
|
||||||
"""
|
|
||||||
|
|
||||||
# catching previously detected long-running code
|
|
||||||
if codestring in self.timedout_codestrings:
|
|
||||||
err = "Code '%s' previously failed with a timeout. Please rewrite code." % codestring
|
|
||||||
if not self.msg(err, scripter, caller):
|
|
||||||
raise EvlangError(err)
|
|
||||||
return
|
|
||||||
|
|
||||||
# dynamically setup context, then overload with custom additions
|
|
||||||
location = None
|
|
||||||
if self.obj:
|
|
||||||
location = self.obj.location
|
|
||||||
context = {"self":self.obj,
|
|
||||||
"caller":caller,
|
|
||||||
"scripter": scripter,
|
|
||||||
"here": location,
|
|
||||||
"evl": Evl(self.obj, caller, scripter)}
|
|
||||||
context.update(self.safe_context)
|
|
||||||
|
|
||||||
# launch the runner in a separate thread, tracking how long it runs.
|
|
||||||
timer = self.start_timer(self.safe_timeout, codestring, scripter, caller)
|
|
||||||
try:
|
|
||||||
yield threads.deferToThread(limited_exec, codestring, context=context,
|
|
||||||
timeout_secs=self.safe_timeout).addCallback(self.stop_timer, timer)
|
|
||||||
except Exception, e:
|
|
||||||
self.stop_timer(None, timer)
|
|
||||||
if not self.msg(e, scripter, caller):
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def run_by_name(self, scriptname, caller=None, quiet=True):
|
|
||||||
"""
|
|
||||||
Run a script previously stored on the handler, identified by scriptname.
|
|
||||||
|
|
||||||
scriptname - identifier of the stored script
|
|
||||||
caller - optional reference to the object triggering the script.
|
|
||||||
quiet - will not raise error if scriptname is not found.
|
|
||||||
|
|
||||||
All scripts run will have access to the self, caller and here variables.
|
|
||||||
"""
|
|
||||||
scripter = None
|
|
||||||
try:
|
|
||||||
codestring, scripter = self.evlang_scripts[scriptname]
|
|
||||||
except KeyError:
|
|
||||||
if quiet:
|
|
||||||
return
|
|
||||||
errmsg = "Found no script with the name '%s'." % scriptname
|
|
||||||
if not self.msg(errmsg, scripter=None, caller=caller):
|
|
||||||
raise EvlangError(errmsg)
|
|
||||||
return
|
|
||||||
# execute code
|
|
||||||
self.run(codestring, caller, scripter)
|
|
||||||
|
|
||||||
def add(self, scriptname, codestring, scripter=None):
|
|
||||||
"""
|
|
||||||
Add a new script to the handler. This will also save the
|
|
||||||
script properly. This is used also to update scripts when
|
|
||||||
debugging.
|
|
||||||
"""
|
|
||||||
self.evlang_scripts[scriptname] = (codestring, scripter)
|
|
||||||
if self.obj:
|
|
||||||
# save to database
|
|
||||||
self.obj.attributes.add(self.evlang_storage_attr,
|
|
||||||
self.evlang_scripts)
|
|
||||||
|
|
||||||
def delete(self, scriptname):
|
|
||||||
"""
|
|
||||||
Permanently remove script from object.
|
|
||||||
"""
|
|
||||||
if scriptname in self.evlang_scripts:
|
|
||||||
del self.evlang_scripts[scriptname]
|
|
||||||
if self.obj:
|
|
||||||
# update change to database
|
|
||||||
self.obj.attributes.add(self.evlang_storage_attr,
|
|
||||||
self.evlang_scripts)
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Limited Python evaluation.
|
|
||||||
|
|
||||||
# Based on PD recipe by Babar K. Zafar
|
|
||||||
# http://code.activestate.com/recipes/496746/
|
|
||||||
|
|
||||||
# Expanded specifically for Evennia by Griatch
|
|
||||||
# - some renaming/cleanup
|
|
||||||
# - limited size of power expressions
|
|
||||||
# - removed print (use msg() instead)
|
|
||||||
# - blocking certain function calls
|
|
||||||
# - removed assignment of properties - this is too big of a security risk.
|
|
||||||
# One needs to us a safe function to change propertes.
|
|
||||||
# - removed thread-based check for execution time - it doesn't work
|
|
||||||
# embedded in twisted/python.
|
|
||||||
# - removed while, since it's night impossible to properly check compile
|
|
||||||
# time in an embedded Python thread (or rather, it's possible, but
|
|
||||||
# there is no way to cancel the thread anyway). while is an easy way
|
|
||||||
# to create an infinite loop.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Module globals.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Toggle module level debugging mode.
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
# List of all AST node classes in _ast.py.
|
|
||||||
ALL_AST_NODES = \
|
|
||||||
set([name for (name, obj) in inspect.getmembers(_ast)
|
|
||||||
if inspect.isclass(obj) and issubclass(obj, _ast.AST)])
|
|
||||||
|
|
||||||
# List of all builtin functions and types (ignoring exception classes).
|
|
||||||
ALL_BUILTINS = \
|
|
||||||
set([name for (name, obj) in inspect.getmembers(__builtin__)
|
|
||||||
if (inspect.isbuiltin(obj) or name in ('True', 'False', 'None') or
|
|
||||||
(inspect.isclass(obj) and not issubclass(obj, BaseException)))])
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Utilties.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def classname(obj):
|
|
||||||
return obj.__class__.__name__
|
|
||||||
|
|
||||||
def is_valid_ast_node(name):
|
|
||||||
return name in ALL_AST_NODES
|
|
||||||
|
|
||||||
def is_valid_builtin(name):
|
|
||||||
return name in ALL_BUILTINS
|
|
||||||
|
|
||||||
def get_node_lineno(node):
|
|
||||||
return (node.lineno) and node.lineno or 0
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Restricted AST nodes & builtins.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Deny evaluation of code if the AST contain any of the following nodes:
|
|
||||||
UNALLOWED_AST_NODES = set([
|
|
||||||
# 'Add', 'And',
|
|
||||||
# 'AssList',
|
|
||||||
# 'AssName',
|
|
||||||
# 'AssTuple',
|
|
||||||
# 'Assert', 'Assign', 'AugAssign',
|
|
||||||
# 'Bitand', 'Bitor', 'Bitxor', 'Break',
|
|
||||||
# 'CallFunc', 'Class', 'Compare', 'Const', 'Continue',
|
|
||||||
# 'Decorators', 'Dict', 'Discard', 'Div',
|
|
||||||
# 'Ellipsis', 'EmptyNode',
|
|
||||||
'Exec',
|
|
||||||
# 'Expression', 'FloorDiv',
|
|
||||||
# 'For',
|
|
||||||
'FunctionDef',
|
|
||||||
# 'GenExpr', 'GenExprFor', 'GenExprIf', 'GenExprInner',
|
|
||||||
# 'Getattr', 'Global', 'If',
|
|
||||||
'Import',
|
|
||||||
'ImportFrom',
|
|
||||||
# 'Invert',
|
|
||||||
# 'Keyword', 'Lambda', 'LeftShift',
|
|
||||||
# 'List', 'ListComp', 'ListCompFor', 'ListCompIf', 'Mod',
|
|
||||||
# 'Module',
|
|
||||||
# 'Mul', 'Name', 'Node', 'Not', 'Or', 'Pass', 'Power',
|
|
||||||
'Print',
|
|
||||||
'Raise',
|
|
||||||
# 'Return', 'RightShift', 'Slice', 'Sliceobj',
|
|
||||||
# 'Stmt', 'Sub', 'Subscript',
|
|
||||||
'TryExcept', 'TryFinally',
|
|
||||||
# 'Tuple', 'UnaryAdd', 'UnarySub',
|
|
||||||
'While',
|
|
||||||
# 'Yield'
|
|
||||||
])
|
|
||||||
|
|
||||||
# Deny evaluation of code if it tries to access any of the following builtins:
|
|
||||||
UNALLOWED_BUILTINS = set([
|
|
||||||
'__import__',
|
|
||||||
# 'abs', 'apply', 'basestring', 'bool', 'buffer',
|
|
||||||
# 'callable', 'chr', 'classmethod', 'cmp', 'coerce',
|
|
||||||
'compile',
|
|
||||||
# 'complex',
|
|
||||||
'delattr',
|
|
||||||
# 'dict',
|
|
||||||
'dir',
|
|
||||||
# 'divmod', 'enumerate',
|
|
||||||
'eval', 'execfile', 'file',
|
|
||||||
# 'filter', 'float', 'frozenset',
|
|
||||||
'getattr', 'globals', 'hasattr',
|
|
||||||
# 'hash', 'hex', 'id',
|
|
||||||
'input',
|
|
||||||
# 'int',
|
|
||||||
'intern',
|
|
||||||
# 'isinstance', 'issubclass', 'iter',
|
|
||||||
# 'len', 'list',
|
|
||||||
'locals',
|
|
||||||
# 'long', 'map', 'max',
|
|
||||||
'memoryview',
|
|
||||||
# 'min', 'object', 'oct',
|
|
||||||
'open',
|
|
||||||
# 'ord', 'pow', 'property', 'range',
|
|
||||||
'raw_input',
|
|
||||||
# 'reduce',
|
|
||||||
'reload',
|
|
||||||
# 'repr', 'reversed', 'round', 'set',
|
|
||||||
'setattr',
|
|
||||||
# 'slice', 'sorted', 'staticmethod', 'str', 'sum',
|
|
||||||
'super',
|
|
||||||
# 'tuple',
|
|
||||||
'type',
|
|
||||||
# 'unichr', 'unicode',
|
|
||||||
'vars',
|
|
||||||
# 'xrange', 'zip'
|
|
||||||
])
|
|
||||||
|
|
||||||
# extra validation whitelist-style to avoid new versions of Python creeping
|
|
||||||
# in with new unsafe things
|
|
||||||
SAFE_BUILTINS = set([
|
|
||||||
'False', 'None', 'True', 'abs', 'all', 'any', 'apply', 'basestring',
|
|
||||||
'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr',
|
|
||||||
'classmethod',
|
|
||||||
'cmp', 'coerce', 'complex', 'dict', 'divmod', 'enumerate', 'filter',
|
|
||||||
'float', 'format', 'frozenset', 'hash', 'hex', 'id', 'int',
|
|
||||||
'isinstance', 'issubclass', 'iter', 'len', 'list', 'long', 'map',
|
|
||||||
'max', 'min',
|
|
||||||
'next', 'object', 'oct', 'ord', 'pow', 'print', 'property', 'range',
|
|
||||||
'reduce',
|
|
||||||
'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'staticmethod',
|
|
||||||
'str',
|
|
||||||
'sum', 'tuple', 'unichr', 'unicode', 'xrange', 'zip'])
|
|
||||||
|
|
||||||
for ast_name in UNALLOWED_AST_NODES:
|
|
||||||
assert(is_valid_ast_node(ast_name))
|
|
||||||
for name in UNALLOWED_BUILTINS:
|
|
||||||
assert(is_valid_builtin(name))
|
|
||||||
|
|
||||||
|
|
||||||
def _cross_match_whitelist():
|
|
||||||
"check the whitelist's completeness"
|
|
||||||
available = ALL_BUILTINS - UNALLOWED_BUILTINS
|
|
||||||
diff = available.difference(SAFE_BUILTINS)
|
|
||||||
assert not diff, diff # check so everything not disallowed is in safe
|
|
||||||
diff = SAFE_BUILTINS.difference(available)
|
|
||||||
assert not diff, diff # check so everything in safe is in not-disallowed
|
|
||||||
_cross_match_whitelist()
|
|
||||||
|
|
||||||
def is_unallowed_ast_node(kind):
|
|
||||||
return kind in UNALLOWED_AST_NODES
|
|
||||||
|
|
||||||
def is_unallowed_builtin(name):
|
|
||||||
return name in UNALLOWED_BUILTINS
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Restricted attributes.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# In addition to these we deny access to all lowlevel attrs (__xxx__).
|
|
||||||
UNALLOWED_ATTR = [
|
|
||||||
'im_class', 'im_func', 'im_self',
|
|
||||||
'func_code', 'func_defaults', 'func_globals', 'func_name',
|
|
||||||
'tb_frame', 'tb_next',
|
|
||||||
'f_back', 'f_builtins', 'f_code', 'f_exc_traceback',
|
|
||||||
'f_exc_type', 'f_exc_value', 'f_globals', 'f_locals']
|
|
||||||
UNALLOWED_ATTR.extend(_EV_UNALLOWED_SYMBOLS)
|
|
||||||
|
|
||||||
|
|
||||||
def is_unallowed_attr(name):
|
|
||||||
return (name[:2] == '__' and name[-2:] == '__') or \
|
|
||||||
(name in UNALLOWED_ATTR)
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# LimitedExecVisitor.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
class LimitedExecError(object):
|
|
||||||
"""
|
|
||||||
Base class for all which occur while walking the AST.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
errmsg = short decription about the nature of the error
|
|
||||||
lineno = line offset to where error occured in source code
|
|
||||||
"""
|
|
||||||
def __init__(self, errmsg, lineno):
|
|
||||||
self.errmsg, self.lineno = errmsg, lineno
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "line %d : %s" % (self.lineno, self.errmsg)
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecASTNodeError(LimitedExecError):
|
|
||||||
"Expression/statement in AST evaluates to a restricted AST node type."
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecBuiltinError(LimitedExecError):
|
|
||||||
"Expression/statement in tried to access a restricted builtin."
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecAttrError(LimitedExecError):
|
|
||||||
"Expression/statement in tried to access a restricted attribute."
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecVisitor(object):
|
|
||||||
"""
|
|
||||||
Data-driven visitor which walks the AST for some code and makes
|
|
||||||
sure it doesn't contain any expression/statements which are
|
|
||||||
declared as restricted in 'UNALLOWED_AST_NODES'. We'll also make
|
|
||||||
sure that there aren't any attempts to access/lookup restricted
|
|
||||||
builtin declared in 'UNALLOWED_BUILTINS'. By default we also won't
|
|
||||||
allow access to lowlevel stuff which can be used to dynamically
|
|
||||||
access non-local envrioments.
|
|
||||||
|
|
||||||
Interface:
|
|
||||||
walk(ast) = validate AST and return True if AST is 'safe'
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
errors = list of LimitedExecError if walk() returned False
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
|
|
||||||
The visitor will automatically generate methods for all of the
|
|
||||||
available AST node types and redirect them to self.ok or self.fail
|
|
||||||
reflecting the configuration in 'UNALLOWED_AST_NODES'. While
|
|
||||||
walking the AST we simply forward the validating step to each of
|
|
||||||
node callbacks which take care of reporting errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"Initialize visitor by generating callbacks for all AST node types."
|
|
||||||
self.errors = []
|
|
||||||
for ast_name in ALL_AST_NODES:
|
|
||||||
# Don't reset any overridden callbacks.
|
|
||||||
if getattr(self, 'visit' + ast_name, None):
|
|
||||||
continue
|
|
||||||
if is_unallowed_ast_node(ast_name):
|
|
||||||
setattr(self, 'visit' + ast_name, self.fail)
|
|
||||||
else:
|
|
||||||
setattr(self, 'visit' + ast_name, self.ok)
|
|
||||||
|
|
||||||
def walk(self, astnode):
|
|
||||||
"Validate each node in AST and return True if AST is 'safe'."
|
|
||||||
self.visit(ast)
|
|
||||||
return self.errors == []
|
|
||||||
|
|
||||||
def visit(self, node, *args):
|
|
||||||
"Recursively validate node and all of its children."
|
|
||||||
fn = getattr(self, 'visit' + classname(node))
|
|
||||||
if DEBUG:
|
|
||||||
self.trace(node)
|
|
||||||
fn(node, *args)
|
|
||||||
for child in node.getChildNodes():
|
|
||||||
self.visit(child, *args)
|
|
||||||
|
|
||||||
def visitName(self, node, *args):
|
|
||||||
"Disallow any attempts to access a restricted builtin/attr."
|
|
||||||
name = node.getChildren()[0]
|
|
||||||
lineno = get_node_lineno(node)
|
|
||||||
if is_unallowed_builtin(name):
|
|
||||||
self.errors.append(LimitedExecBuiltinError(
|
|
||||||
"access to builtin '%s' is denied" % name, lineno))
|
|
||||||
elif is_unallowed_attr(name):
|
|
||||||
self.errors.append(LimitedExecAttrError(
|
|
||||||
"access to attribute '%s' is denied" % name, lineno))
|
|
||||||
|
|
||||||
def visitGetattr(self, node, *args):
|
|
||||||
"Disallow any attempts to access a restricted attribute."
|
|
||||||
attrname = node.attrname
|
|
||||||
try:
|
|
||||||
name = node.getChildren()[0].name
|
|
||||||
except Exception:
|
|
||||||
name = ""
|
|
||||||
lineno = get_node_lineno(node)
|
|
||||||
if attrname == 'attr' and name == 'evl':
|
|
||||||
pass
|
|
||||||
elif is_unallowed_attr(attrname):
|
|
||||||
self.errors.append(LimitedExecAttrError(
|
|
||||||
"access to attribute '%s' is denied" % attrname, lineno))
|
|
||||||
|
|
||||||
def visitAssName(self, node, *args):
|
|
||||||
"Disallow attempts to delete an attribute or name"
|
|
||||||
if node.flags == 'OP_DELETE':
|
|
||||||
self.fail(node, *args)
|
|
||||||
|
|
||||||
def visitPower(self, node, *args):
|
|
||||||
"Make sure power-of operations don't get too big"
|
|
||||||
if node.left.value > 1000000 or node.right.value > 10:
|
|
||||||
lineno = get_node_lineno(node)
|
|
||||||
self.errors.append(LimitedExecAttrError(
|
|
||||||
"power law solution too big - restricted", lineno))
|
|
||||||
|
|
||||||
def ok(self, node, *args):
|
|
||||||
"Default callback for 'harmless' AST nodes."
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fail(self, node, *args):
|
|
||||||
"Default callback for unallowed AST nodes."
|
|
||||||
lineno = get_node_lineno(node)
|
|
||||||
self.errors.append(LimitedExecASTNodeError(
|
|
||||||
"execution of '%s' statements is denied" % classname(node),
|
|
||||||
lineno))
|
|
||||||
|
|
||||||
def trace(self, node):
|
|
||||||
"Debugging utility for tracing the validation of AST nodes."
|
|
||||||
print classname(node)
|
|
||||||
for attr in dir(node):
|
|
||||||
if attr[:2] != '__':
|
|
||||||
print ' ' * 4, "%-15.15s" % attr, getattr(node, attr)
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Safe 'eval' replacement.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
class LimitedExecException(Exception):
|
|
||||||
"Base class for all safe-eval related errors."
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecCodeException(LimitedExecException):
|
|
||||||
"""
|
|
||||||
Exception class for reporting all errors which occured while
|
|
||||||
validating AST for source code in limited_exec().
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
code = raw source code which failed to validate
|
|
||||||
errors = list of LimitedExecError
|
|
||||||
"""
|
|
||||||
def __init__(self, code, errors):
|
|
||||||
self.code, self.errors = code, errors
|
|
||||||
def __str__(self):
|
|
||||||
return '\n'.join([str(err) for err in self.errors])
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecContextException(LimitedExecException):
|
|
||||||
"""
|
|
||||||
Exception class for reporting unallowed objects found in the dict
|
|
||||||
intended to be used as the local enviroment in safe_eval().
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
keys = list of keys of the unallowed objects
|
|
||||||
errors = list of strings describing the nature of the error
|
|
||||||
for each key in 'keys'
|
|
||||||
"""
|
|
||||||
def __init__(self, keys, errors):
|
|
||||||
self.keys, self.errors = keys, errors
|
|
||||||
def __str__(self):
|
|
||||||
return '\n'.join([str(err) for err in self.errors])
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedExecTimeoutException(LimitedExecException):
|
|
||||||
"""
|
|
||||||
Exception class for reporting that code evaluation execeeded
|
|
||||||
the given timelimit.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
timeout = time limit in seconds
|
|
||||||
"""
|
|
||||||
def __init__(self, timeout):
|
|
||||||
self.timeout = timeout
|
|
||||||
def __str__(self):
|
|
||||||
return "Timeout limit execeeded (%s secs) during exec" % self.timeout
|
|
||||||
|
|
||||||
|
|
||||||
def validate_context(context):
|
|
||||||
"Checks a supplied context for dangerous content"
|
|
||||||
ctx_errkeys, ctx_errors = [], []
|
|
||||||
for (key, obj) in context.items():
|
|
||||||
if inspect.isbuiltin(obj):
|
|
||||||
ctx_errkeys.append(key)
|
|
||||||
ctx_errors.append("key '%s' : unallowed builtin %s" % (key, obj))
|
|
||||||
if inspect.ismodule(obj):
|
|
||||||
ctx_errkeys.append(key)
|
|
||||||
ctx_errors.append("key '%s' : unallowed module %s" % (key, obj))
|
|
||||||
|
|
||||||
if ctx_errors:
|
|
||||||
raise LimitedExecContextException(ctx_errkeys, ctx_errors)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def validate_code(codestring):
|
|
||||||
"validate a code string"
|
|
||||||
# prepare the code tree for checking
|
|
||||||
astnode = ast.parse(codestring)
|
|
||||||
checker = LimitedExecVisitor()
|
|
||||||
|
|
||||||
# check code tree, then execute in a time-restricted environment
|
|
||||||
if not checker.walk(astnode):
|
|
||||||
raise LimitedExecCodeException(codestring, checker.errors)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def limited_exec(code, context = {}, timeout_secs=2, retobj=None, procpool_async=None):
|
|
||||||
"""
|
|
||||||
Validate source code and make sure it contains no unauthorized
|
|
||||||
expression/statements as configured via 'UNALLOWED_AST_NODES' and
|
|
||||||
'UNALLOWED_BUILTINS'. By default this means that code is not
|
|
||||||
allowed import modules or access dangerous builtins like 'open' or
|
|
||||||
'eval'.
|
|
||||||
|
|
||||||
code - code to execute. Will be evaluated for safety
|
|
||||||
context - if code is deemed safe, code will execute with this environment
|
|
||||||
time_out_secs - only used if procpool_async is given. Sets timeout
|
|
||||||
for remote code execution
|
|
||||||
retobj - only used if procpool_async is also given. Defines an Object
|
|
||||||
(which must define a msg() method), for receiving returns from
|
|
||||||
the execution.
|
|
||||||
procpool_async - a run_async function alternative to the one in
|
|
||||||
src.utils.utils. This must accept the keywords
|
|
||||||
proc_timeout (will be set to timeout_secs
|
|
||||||
at_return - a callback
|
|
||||||
at_err - an errback
|
|
||||||
If retobj is given, at_return/at_err will be created and
|
|
||||||
set to msg callbacks and errors to that object.
|
|
||||||
Tracebacks:
|
|
||||||
LimitedExecContextException
|
|
||||||
LimitedExecCodeException
|
|
||||||
"""
|
|
||||||
if validate_context(context) and validate_code(code):
|
|
||||||
# run code only after validation has completed
|
|
||||||
if procpool_async:
|
|
||||||
# custom run_async
|
|
||||||
if retobj:
|
|
||||||
callback = lambda r: retobj.msg(r)
|
|
||||||
errback = lambda e: retobj.msg(e)
|
|
||||||
procpool_async(code, *context,
|
|
||||||
proc_timeout=timeout_secs,
|
|
||||||
at_return=callback,
|
|
||||||
at_err=errback)
|
|
||||||
else:
|
|
||||||
procpool_async(code, *context, proc_timeout=timeout_secs)
|
|
||||||
else:
|
|
||||||
# run in-process
|
|
||||||
exec code in context
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
# Basic tests.
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
class TestLimitedExec(unittest.TestCase):
|
|
||||||
def test_builtin(self):
|
|
||||||
# attempt to access a unsafe builtin
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "open('test.txt', 'w')")
|
|
||||||
|
|
||||||
def test_getattr(self):
|
|
||||||
# attempt to get arround direct attr access
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "getattr(int, '__abs__')")
|
|
||||||
|
|
||||||
def test_func_globals(self):
|
|
||||||
# attempt to access global enviroment where fun was defined
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "def x(): pass; print x.func_globals")
|
|
||||||
|
|
||||||
def test_lowlevel(self):
|
|
||||||
# lowlevel tricks to access 'object'
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "().__class__.mro()[1].__subclasses__()")
|
|
||||||
|
|
||||||
def test_timeout_ok(self):
|
|
||||||
# attempt to exectute 'slow' code which finishes within timelimit
|
|
||||||
def test(): time.sleep(2)
|
|
||||||
env = {'test': test}
|
|
||||||
limited_exec("test()", env, timeout_secs=5)
|
|
||||||
|
|
||||||
def test_timeout_exceed(self):
|
|
||||||
# attempt to exectute code which never teminates
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "while 1: pass")
|
|
||||||
|
|
||||||
def test_invalid_context(self):
|
|
||||||
# can't pass an enviroment with modules or builtins
|
|
||||||
env = {'f': __builtins__.open, 'g': time}
|
|
||||||
self.assertRaises(LimitedExecException,
|
|
||||||
limited_exec, "print 1", env)
|
|
||||||
|
|
||||||
def test_callback(self):
|
|
||||||
# modify local variable via callback
|
|
||||||
self.value = 0
|
|
||||||
def test(): self.value = 1
|
|
||||||
env = {'test': test}
|
|
||||||
limited_exec("test()", env)
|
|
||||||
self.assertEqual(self.value, 1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
Evlang - usage examples
|
|
||||||
|
|
||||||
Craftable object with matching command
|
|
||||||
|
|
||||||
Evennia contribution - Griatch 2012
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ev import create_object
|
|
||||||
from ev import default_cmds
|
|
||||||
from contrib.evlang.objects import ScriptableObject
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Example for creating a scriptable object with a custom
|
|
||||||
# "crafting" command that sets coding restrictions on the
|
|
||||||
# object.
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
class CmdCraftScriptable(default_cmds.MuxCommand):
|
|
||||||
"""
|
|
||||||
craft a scriptable object
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
@craftscriptable <name>
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "@craftscriptable"
|
|
||||||
locks = "cmd:perm(Builder)"
|
|
||||||
help_category = "Building"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"Implements the command"
|
|
||||||
caller = self.caller
|
|
||||||
if not self.args:
|
|
||||||
caller.msg("Usage: @craftscriptable <name>")
|
|
||||||
return
|
|
||||||
objname = self.args.strip()
|
|
||||||
obj = create_object(CraftedScriptableObject, key=objname, location=caller.location)
|
|
||||||
if not obj:
|
|
||||||
caller.msg("There was an error creating %s!" % objname)
|
|
||||||
return
|
|
||||||
# set locks on the object restrictive coding only to us, the creator.
|
|
||||||
obj.db.evlang_locks = {"get":"code:id(%s) or perm(Wizards)" % caller.dbref,
|
|
||||||
"drop":"code:id(%s) or perm(Wizards)" % caller.dbref,
|
|
||||||
"look": "code:id(%s) or perm(Wizards)" % caller.dbref}
|
|
||||||
caller.msg("Crafted %s. Use @desc and @code to customize it." % objname)
|
|
||||||
|
|
||||||
|
|
||||||
class CraftedScriptableObject(ScriptableObject):
|
|
||||||
"""
|
|
||||||
An object which allows customization of what happens when it is
|
|
||||||
dropped, taken or examined. It is meant to be created with the
|
|
||||||
special command CmdCraftScriptable above, for example as part of
|
|
||||||
an in-game "crafting" operation. It can henceforth be expanded
|
|
||||||
with custom scripting with the @code command (and only the crafter
|
|
||||||
(and Wizards) will be able to do so).
|
|
||||||
|
|
||||||
Allowed Evlang scripts:
|
|
||||||
"get"
|
|
||||||
"drop"
|
|
||||||
"look"
|
|
||||||
"""
|
|
||||||
def at_get(self, getter):
|
|
||||||
"called when object is picked up"
|
|
||||||
self.ndb.evlang.run_by_name("get", getter)
|
|
||||||
def at_drop(self, dropper):
|
|
||||||
"called when object is dropped"
|
|
||||||
self.ndb.evlang.run_by_name("drop", dropper)
|
|
||||||
def at_desc(self, looker):
|
|
||||||
"called when object is being looked at."
|
|
||||||
self.ndb.evlang.run_by_name("look", looker)
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
Evlang usage examples
|
|
||||||
scriptable Evennia base typeclass and @code command
|
|
||||||
|
|
||||||
Evennia contribution - Griatch 2012
|
|
||||||
|
|
||||||
The ScriptableObject typeclass initiates the Evlang handler on
|
|
||||||
itself as well as sets up a range of commands to
|
|
||||||
allow for scripting its functionality. It sets up an access
|
|
||||||
control system using the 'code' locktype to limit access to
|
|
||||||
these codes.
|
|
||||||
|
|
||||||
The @code command allows to add scripted evlang code to
|
|
||||||
a ScriptableObject. It will handle access checks.
|
|
||||||
|
|
||||||
|
|
||||||
There are also a few examples of usage - a simple Room
|
|
||||||
object that has scriptable behaviour when it is being entered
|
|
||||||
as well as a more generic template for a Craftable object along
|
|
||||||
with a base crafting command to create it and set it up with
|
|
||||||
access restrictions making it only scriptable by the original
|
|
||||||
creator.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from contrib.evlang import evlang
|
|
||||||
from src.locks.lockhandler import LockHandler
|
|
||||||
from ev import Object
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
# Typeclass bases
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
|
||||||
class ScriptableObject(Object):
|
|
||||||
"""
|
|
||||||
Base class for an object possible to script. By default it defines
|
|
||||||
no scriptable types.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def init_evlang(self):
|
|
||||||
"""
|
|
||||||
Initialize an Evlang handler with access control. Requires
|
|
||||||
the evlang_locks attribute to be set to a dictionary with
|
|
||||||
{name:lockstring, ...}.
|
|
||||||
"""
|
|
||||||
evl = evlang.Evlang(self)
|
|
||||||
evl.lock_storage = ""
|
|
||||||
evl.lockhandler = LockHandler(evl)
|
|
||||||
for lockstring in self.db.evlang_locks.values():
|
|
||||||
evl.lockhandler.add(lockstring)
|
|
||||||
return evl
|
|
||||||
|
|
||||||
def at_object_creation(self):
|
|
||||||
"""
|
|
||||||
We add the Evlang handler and sets up
|
|
||||||
the needed properties.
|
|
||||||
"""
|
|
||||||
# this defines the available types along with the lockstring
|
|
||||||
# restricting access to them. Anything not defined in this
|
|
||||||
# dictionary is forbidden to script at all. Just because
|
|
||||||
# a script type is -available- does not mean there is any
|
|
||||||
# code yet in that slot!
|
|
||||||
self.db.evlang_locks = {}
|
|
||||||
# This stores actual code snippets. Only code with codetypes
|
|
||||||
# matching the keys in db.evlang_locks will work.
|
|
||||||
self.db.evlang_scripts = {}
|
|
||||||
# store Evlang handler non-persistently
|
|
||||||
self.ndb.evlang = self.init_evlang()
|
|
||||||
|
|
||||||
def at_init(self):
|
|
||||||
"We must also re-add the handler at server reboots"
|
|
||||||
self.ndb.evlang = self.init_evlang()
|
|
||||||
|
|
||||||
# Example object types
|
|
||||||
|
|
||||||
from ev import Room
|
|
||||||
class ScriptableRoom(Room, ScriptableObject):
|
|
||||||
"""
|
|
||||||
A room that is scriptable as well as allows users
|
|
||||||
to script what happens when users enter it.
|
|
||||||
|
|
||||||
Allowed scripts:
|
|
||||||
"enter" (allowed to be modified by all builders)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def at_object_creation(self):
|
|
||||||
"initialize the scriptable object"
|
|
||||||
self.db.evlang_locks = {"enter": "code:perm(Builders)"}
|
|
||||||
self.db.evlang_scripts = {}
|
|
||||||
self.ndb.evlang = self.init_evlang()
|
|
||||||
|
|
||||||
def at_object_receive(self, obj, source_location):
|
|
||||||
"fires a script of type 'enter' (no error if it's not defined)"
|
|
||||||
self.ndb.evlang.run_by_name("enter", obj)
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue