CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

219 lines
7.0 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2017 Adam Hartz <hartz@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import re
  18. import ast
  19. import resource
  20. import subprocess
  21. def _execfile(*args):
  22. fn = args[0]
  23. with open(fn) as f:
  24. c = compile(f.read(), fn, 'exec')
  25. exec(c, *args[1:])
  26. def prep_code(code, test, **kwargs):
  27. # code is whatever code we need to test; test is the dictionary describing
  28. # what test we should be running
  29. code = code.strip()
  30. if kwargs.get('csq_python3', True):
  31. footer = ('print("!LOGOUTPUT(o_O)!")\n'
  32. 'print(repr(%s))\n') % test['variable']
  33. else:
  34. footer = ('print "!LOGOUTPUT(o_O)!"\n'
  35. 'print repr(%s)\n') % test['variable']
  36. code = '\n\n'.join((kwargs['csq_code_pre'], code, kwargs['csq_code_post'],
  37. test['code'], footer))
  38. return code
  39. def sandbox_run_code(context, code, options):
  40. s = context.get('csq_python_sandbox', 'remote')
  41. sandbox_file = os.path.join(context['cs_fs_root'], '__QTYPES__',
  42. 'pythoncode', '__SANDBOXES__', '%s.py' % s)
  43. opts = dict(DEFAULT_OPTIONS)
  44. opts.update(options)
  45. sandbox = dict(context)
  46. _execfile(sandbox_file, sandbox)
  47. return sandbox['run_code'](context, code, opts)
  48. def fix_error_msg(fname, err, offset, sub):
  49. sublen = sub.count('\n')
  50. def subber(m):
  51. g = m.groups()
  52. out = g[0]
  53. lineno = int(g[1])
  54. if lineno > (offset + sublen + 1):
  55. #error in test code
  56. out += 'File <Test Code>, line %d%s' % (lineno, g[2])
  57. elif lineno < offset:
  58. #error in test code
  59. out += 'File <Test Code Preamble>, line %d%s' % (lineno, g[2])
  60. else:
  61. #error in user-submitted code
  62. out += 'File <User-Submitted Code>, line %d%s' % (lineno - offset,
  63. g[2])
  64. return out
  65. error_regex = re.compile('(.*?)File "%s", line ([0-9]+)(,?[\n]*)' % fname)
  66. err = error_regex.sub(subber, err)
  67. e = err.split('\n')
  68. if len(e) > 10:
  69. err = '...ERROR MESSAGE TRUNCATED...\n\n' + '\n'.join(e[-10:])
  70. err = err.replace('[Subprocess exit code: 1]', '')
  71. err = re.compile('(.*?)File "app_main.py", line ([0-9]+)(,?[^\n]*)\n').sub(
  72. '', err)
  73. return err
  74. DEFAULT_OPTIONS = {
  75. 'CPUTIME': 1,
  76. 'CLOCKTIME': 1,
  77. 'MEMORY': 32 * 1024**2,
  78. 'FILESIZE': 0,
  79. 'BADIMPORT': [],
  80. 'BADVAR': [],
  81. 'FILES': [],
  82. 'STDIN': '',
  83. }
  84. def sandbox_run_test(context, code, test):
  85. options = dict(DEFAULT_OPTIONS)
  86. options.update(context.get('csq_sandbox_options', {}))
  87. options.update(test.get('options', {}))
  88. safe = safety_check(code, options['BADIMPORT'], options['BADVAR'])
  89. if isinstance(safe, tuple):
  90. return ('', ('On line %d: ' % safe[0]) + safe[1], '')
  91. fname, out, err = sandbox_run_code(context, prep_code(code, test, **
  92. context), options)
  93. err = fix_error_msg(fname, err, context['csq_code_pre'].count('\n') + 2,
  94. code)
  95. n = out.split("!LOGOUTPUT(o_O)!")
  96. if len(n) == 2: # should be this
  97. out, log = n
  98. elif len(n) == 1: # code didn't run to completion
  99. if err.strip() == "":
  100. err = ("Your code did not run to completion, "
  101. "but no error message was returned."
  102. "\nThis normally means that your code contains an "
  103. "infinite loop or otherwise took too long to run.")
  104. log = ''
  105. else: # ???
  106. out = ''
  107. log = ''
  108. err = "BAD CODE - this will be logged"
  109. outlines = out.split('\n')
  110. if len(outlines) > 10:
  111. outlines = outlines[:10] + ['...OUTPUT TRUNCATED...']
  112. out = '\n'.join(outlines)
  113. if len(out) >= 5000:
  114. out = out[:5000] + "\n\n...OUTPUT TRUNCATED..."
  115. return out.strip(), err.strip(), log.strip()
  116. def _ast_downward_search(node, testfunc):
  117. """
  118. recursive search through AST. if a node causes testfunc to return true,
  119. then return that. otherwise, return None
  120. """
  121. out = []
  122. if testfunc(node):
  123. out.append(node)
  124. for i in node._hz_children:
  125. out.extend(_ast_downward_search(i, testfunc))
  126. return out
  127. def _prepare_ast(tree, parent=None):
  128. """
  129. stupid little piece of code to add a parent pointer to all nodes
  130. """
  131. tree._hz_parent = parent
  132. tree._hz_children = list(ast.iter_child_nodes(tree))
  133. for i in tree._hz_children:
  134. _prepare_ast(i, tree)
  135. def _blacklist_variable(var, blacklist=None):
  136. blacklist = blacklist or []
  137. if var.id in blacklist:
  138. return "Disallowed variable name: %s" % var.id
  139. else:
  140. return None
  141. def _blacklist_import(imp, blacklist=None):
  142. blacklist = blacklist or []
  143. if isinstance(imp, ast.ImportFrom):
  144. for i in blacklist:
  145. if re.match(i, imp.module):
  146. return "Disallowed import from %s" % imp.module
  147. else:
  148. # is Import instance
  149. for n in [i.name for i in imp.names]:
  150. for i in blacklist:
  151. if re.match(i, n):
  152. return "Disallowed import: %s" % n
  153. def safety_check(code, bad_imports=None, bad_variables=None):
  154. """
  155. Return None if the code is fine; otherise, return (lineno, errmsg)
  156. """
  157. code = code.replace('\r\n', '\n').strip() # whitespace issues...
  158. # parse code down into AST. return error message on failure
  159. try:
  160. tree = ast.parse(code)
  161. _prepare_ast(
  162. tree) # recursively add parent/child pointers to each node
  163. except:
  164. return "SYNTAX ERROR" # TODO: replace this with real error message
  165. # collect all instances of Name not contained in an Attribute object
  166. search = _ast_downward_search
  167. vars = search(
  168. tree,
  169. lambda n: (isinstance(n, ast.Name) and (not isinstance(n._hz_parent, ast.Attribute)) and (not (isinstance(n._hz_parent, ast.Assign) and n in n._hz_parent.targets))))
  170. for var in (vars or []):
  171. res = _blacklist_variable(var, bad_variables)
  172. if res is not None:
  173. return (var.lineno, res)
  174. # collect all imports
  175. imports = _ast_downward_search(
  176. tree,
  177. lambda n: (isinstance(n, ast.Import) or isinstance(n, ast.ImportFrom)))
  178. for imp in (imports or []):
  179. res = _blacklist_import(imp, bad_imports)
  180. if res is not None:
  181. return (imp.lineno, res)