diff --git a/plugins/SupySandbox/README.txt b/plugins/SupySandbox/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/plugins/SupySandbox/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/plugins/SupySandbox/__init__.py b/plugins/SupySandbox/__init__.py new file mode 100644 index 000000000..5e5a07162 --- /dev/null +++ b/plugins/SupySandbox/__init__.py @@ -0,0 +1,68 @@ +# -*- coding: utf8 -*- +### +# Copyright (c) 2010, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +""" +Ce plugin est un portage du robot IRC Fschfsch, servant à fournir un accès +à la pysandbox sur IRC. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "0.1" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.Author('Valentin Lorentz', 'ProgVal', + 'progval@gmail.com') + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = 'http://supybot-fr.tk/SupySandbox' + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/SupySandbox/config.py b/plugins/SupySandbox/config.py new file mode 100644 index 000000000..748065cad --- /dev/null +++ b/plugins/SupySandbox/config.py @@ -0,0 +1,50 @@ +# -*- coding: utf8 -*- +### +# Copyright (c) 2010, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('SupySandbox', True) + + +PySandbox = conf.registerPlugin('SupySandbox') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(PySandbox, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/SupySandbox/local/__init__.py b/plugins/SupySandbox/local/__init__.py new file mode 100644 index 000000000..e86e97b86 --- /dev/null +++ b/plugins/SupySandbox/local/__init__.py @@ -0,0 +1 @@ +# Stub so local is a module, used for third-party modules diff --git a/plugins/SupySandbox/local/fschfsch.py b/plugins/SupySandbox/local/fschfsch.py new file mode 100644 index 000000000..20872fae6 --- /dev/null +++ b/plugins/SupySandbox/local/fschfsch.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +# this file is under the WTFPLv2 [http://sam.zoy.org/wtfpl] +# v1: 2010/05/23 +# Author: Tila + +# You need a configuration file: ~/.fschfsch.py. Config example: +# --- +# host = 'irc.freenode.net' +# port = 7000 +# ssl = True +# nickname = 'botnickname' +# password = 'secret' +# channels = ['##fschfsch', '#channel2', '#channel3'] +# texts = {'help': 'I am fschfsch, a robot snake that evals python code', +# 'sandbox': "I am powered by setrlimit and pysandbox [http://github.com/haypo/pysandbox], I don't fear you"} +# --- + +''' +fschfsch is a Python-evaluating bot. fschfsch is pronounced "fssshh! fssshh!". +''' + +IN_MAXLEN = 300 # bytes +OUT_MAXLEN = 300 # bytes +TIMEOUT = 3 # seconds + +EVAL_MAXTIMESECONDS = TIMEOUT +EVAL_MAXMEMORYBYTES = 10 * 1024 * 1024 # 10 MiB + + +try: + import sandbox as S +except ImportError: + print 'You need pysandbox in order to run fschfsch [http://github.com/haypo/pysandbox].' + raise +try: + import twisted +except ImportError: + print 'You need twisted in order to run fschfsch.' + raise +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet import ssl, reactor +from twisted.words.im.ircsupport import IRCProto +from twisted.words.protocols.irc import IRCClient +# other imports +import re +import sys +import os +import resource as R +import select +import signal +import time +import threading +import random + +def createSandboxConfig(): + cfg = S.SandboxConfig( + 'stdout', + 'stderr', + 'regex', + 'unicodedata', # flow wants u'\{ATOM SYMBOL}' :-) + 'future', + 'code', + 'time', + 'datetime', + 'math', + 'itertools', + 'random', + 'encodings', + ) + cfg.allowModule('sys', + 'version', 'hexversion', 'version_info') + return cfg + +def _evalPython(line, locals): + locals = dict(locals) + try: + if "\n" in line: + raise SyntaxError() + code = compile(line, "", "single") + except SyntaxError: + code = compile(line, "", "exec") + exec code in locals + +def evalPython(line, locals=None): + sandbox = S.Sandbox(config=createSandboxConfig()) + + if locals is not None: + locals = dict(locals) + else: + locals = dict() + try: + sandbox.call(_evalPython, line, locals) + except BaseException, e: + print 'Error: [%s] %s' % (e.__class__.__name__, str(e)) + except: + print 'Error: ' + sys.stdout.flush() + +def childProcess(line, w, locals): + # reseed after a fork to avoid generating the same sequence for each child + random.seed() + + sys.stdout = sys.stderr = os.fdopen(w, 'w') + + R.setrlimit(R.RLIMIT_CPU, (EVAL_MAXTIMESECONDS, EVAL_MAXTIMESECONDS)) + R.setrlimit(R.RLIMIT_AS, (EVAL_MAXMEMORYBYTES, EVAL_MAXMEMORYBYTES)) + R.setrlimit(R.RLIMIT_NPROC, (0, 0)) # 0 forks + + evalPython(line, locals) + +def handleChild(childpid, r): + txt = '' + if any(select.select([r], [], [], TIMEOUT)): + txt = os.read(r, OUT_MAXLEN + 1) + os.close(r) + if OUT_MAXLEN < len(txt): + txt = txt[:OUT_MAXLEN] + '...' + + n = 0 + while n < 6: + pid, status = os.waitpid(childpid, os.WNOHANG) + if pid: + break + time.sleep(.5) + n += 1 + if not pid: + os.kill(childpid, signal.SIGKILL) + return 'Timeout' + elif os.WIFEXITED(status): + txts = txt.rstrip().split('\n') + if len(txts) > 1: + txt = txts[0].rstrip() + ' [+ %d line(s)]' % (len(txts) - 1) + else: + txt = txts[0].rstrip() + return 'Output: ' + txt + elif os.WIFSIGNALED(status): + return 'Killed' + + + +class EvalJob(threading.Thread): + def __init__(self, line, irc, channel): + super(EvalJob, self).__init__() + self.line = line + self.irc = irc + self.channel = channel + + def run(self): + output = self.handle_line(self.line) + reactor.callFromThread(self.irc.say, self.channel, output) + self.irc.executionLock.release() + + def handle_line(self, line): + if IN_MAXLEN < len(line): + return '(command is too long: %s bytes, the maximum is %s)' % (len(line), IN_MAXLEN) + + print("Process %s" % repr(line)) + r, w = os.pipe() + childpid = os.fork() + if not childpid: + os.close(r) + childProcess(line, w, self.irc.factory.morevars) + os._exit(0) + else: + os.close(w) + result = handleChild(childpid, r) + print("=> %s" % repr(result)) + return result + + + +class EvalBot(IRCClient): + versionName = 'fschfsch' + versionNum = '0.1' + + #~ def __init__(self, *a, **k): + def connectionMade(self): + self.nickname = self.factory.nick + self.password = self.factory.password + self.talkre = re.compile('^%s[>:,] (.*)$' % self.nickname) + + self.executionLock = threading.Semaphore() + self.pingSelfId = None + + IRCClient.connectionMade(self) + + def signedOn(self): + self.pingSelfId = reactor.callLater(180, self.pingSelf) + for chan in self.factory.channels: + self.join(chan) + + def pingSelf(self): + # used to avoid some timeouts where fschfsch does not reconnect + self.ping(self.nickname) + self.pingSelfId = reactor.callLater(180, self.pingSelf) + + def privmsg(self, user, channel, message): + if self.pingSelfId is not None: + self.pingSelfId.reset(180) + if user.startswith('haypo') and message.startswith('exit'): + os._exit(0) + if not channel: + return + if not message.startswith(self.nickname): + return + if not self.talkre.match(message): + return + if not self.executionLock.acquire(blocking=False): + return + + pyline = self.talkre.match(message).group(1) + pyline = pyline.replace(' $$ ', '\n') + + self.handleThread = EvalJob(pyline, self, channel) + self.handleThread.start() + + +class MyFactory(ReconnectingClientFactory): + def __init__(self, **kw): + for k in kw: + if k in ('nick', 'password', 'channels', 'morevars'): + setattr(self, k, kw[k]) + protocol = EvalBot + +def check_output(expr, expected, locals=None): + from cStringIO import StringIO + original_stdout = sys.stdout + try: + output = StringIO() + sys.stdout = output + evalPython(expr, locals) + stdout = output.getvalue() + assert stdout == expected, "%r != %r" % (stdout, expected) + finally: + sys.stdout = original_stdout + +def runTests(): + # single + check_output('1+1', '2\n') + check_output('1; 2', '1\n2\n') + check_output( + # written in a single line + "prime=lambda n,i=2:" + "False if n%i==0 else prime(n,i+1) if i*i", "single") + except SyntaxError: + code = compile(line, "", "exec") + exec code in locals + +def evalPython(line, locals=None): + sandbox = S.Sandbox(config=createSandboxConfig()) + + if locals is not None: + locals = dict(locals) + else: + locals = dict() + try: + sandbox.call(_evalPython, line, locals) + except BaseException, e: + print 'Error: [%s] %s' % (e.__class__.__name__, str(e)) + except: + print 'Error: ' + sys.stdout.flush() + +def check_output(expr, expected, locals=None): + from cStringIO import StringIO + original_stdout = sys.stdout + try: + output = StringIO() + sys.stdout = output + evalPython(expr, locals) + stdout = output.getvalue() + assert stdout == expected, "%r != %r" % (stdout, expected) + finally: + sys.stdout = original_stdout + +def runTests(): + # single + check_output('1+1', '2\n') + check_output('1; 2', '1\n2\n') + check_output( + # written in a single line + "prime=lambda n,i=2:" + "False if n%i==0 else prime(n,i+1) if i*i.*)') + def sandbox(self, irc, msg, args): + """ + + Runs Python code safely thanks to pysandbox""" + code = self._parser.match(msg.args[1]).group('code') + irc.reply(handle_line(code.replace(' $$ ', '\n'))) + + def runtests(self, irc, msg, args): + irc.reply(runTests()) + + +Class = SupySandbox + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/plugins/SupySandbox/test.py b/plugins/SupySandbox/test.py new file mode 100644 index 000000000..abf415165 --- /dev/null +++ b/plugins/SupySandbox/test.py @@ -0,0 +1,56 @@ +# -*- coding: utf8 -*- +### +# Copyright (c) 2010, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +from supybot.test import * + +class SupySandboxTestCase(PluginTestCase): + plugins = ('SupySandbox',) + + def testFschfschTestcase(self): + self.assertResponse('runtests', 'True') + + def testCodeIsSuccessfullyRunned(self): + self.assertResponse('sandbox 1+1', "2") + self.assertResponse('sandbox print 1+1', "2") + self.assertResponse('sandbox print \'toto\'', "toto") + + def testMultine(self): + self.assertResponse('sandbox print 1; print 2', "'1\\n2'") + self.assertResponse('sandbox print 1 $$ print 2', "'1\\n2'") + self.assertResponse('sandbox toto=True $$ while toto: $$ print "foo"' + ' $$ toto=False', "foo") + + def testProtections(self): + #self.assertResponse('sandbox while True: print 1', "Timeout") + pass + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: