332 lines
10 KiB
Python
332 lines
10 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
| This file is part of the web2py Web Framework
|
|
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
|
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
|
|
|
Restricted environment to execute application's code
|
|
-----------------------------------------------------
|
|
"""
|
|
|
|
import sys
|
|
from gluon._compat import pickle, ClassType, unicodeT, to_bytes
|
|
import traceback
|
|
import types
|
|
import os
|
|
import logging
|
|
|
|
from gluon.storage import Storage
|
|
from gluon.http import HTTP
|
|
from gluon.html import BEAUTIFY, XML
|
|
from gluon.settings import global_settings
|
|
|
|
logger = logging.getLogger("web2py")
|
|
|
|
__all__ = ['RestrictedError', 'restricted', 'TicketStorage', 'compile2']
|
|
|
|
|
|
class TicketStorage(Storage):
|
|
|
|
"""
|
|
Defines the ticket object and the default values of its members (None)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
db=None,
|
|
tablename='web2py_ticket'
|
|
):
|
|
Storage.__init__(self)
|
|
self.db = db
|
|
self.tablename = tablename
|
|
|
|
def store(self, request, ticket_id, ticket_data):
|
|
"""
|
|
Stores the ticket. It will figure out if this must be on disk or in db
|
|
"""
|
|
if self.db:
|
|
self._store_in_db(request, ticket_id, ticket_data)
|
|
else:
|
|
self._store_on_disk(request, ticket_id, ticket_data)
|
|
|
|
def _store_in_db(self, request, ticket_id, ticket_data):
|
|
self.db._adapter.reconnect()
|
|
try:
|
|
table = self._get_table(self.db, self.tablename, request.application)
|
|
table.insert(ticket_id=ticket_id,
|
|
ticket_data=pickle.dumps(ticket_data, pickle.HIGHEST_PROTOCOL),
|
|
created_datetime=request.now)
|
|
self.db.commit()
|
|
message = 'In FILE: %(layer)s\n\n%(traceback)s\n'
|
|
except Exception:
|
|
self.db.rollback()
|
|
message =' Unable to store in FILE: %(layer)s\n\n%(traceback)s\n'
|
|
self.db.close()
|
|
logger.error(message % ticket_data)
|
|
|
|
def _store_on_disk(self, request, ticket_id, ticket_data):
|
|
ef = self._error_file(request, ticket_id, 'wb')
|
|
try:
|
|
pickle.dump(ticket_data, ef)
|
|
finally:
|
|
ef.close()
|
|
|
|
def _error_file(self, request, ticket_id, mode, app=None):
|
|
root = request.folder
|
|
if app:
|
|
root = os.path.join(os.path.join(root, '..'), app)
|
|
errors_folder = os.path.abspath(
|
|
os.path.join(root, 'errors')) # .replace('\\', '/')
|
|
return open(os.path.join(errors_folder, ticket_id), mode)
|
|
|
|
def _get_table(self, db, tablename, app):
|
|
tablename = tablename + '_' + app
|
|
table = db.get(tablename)
|
|
if not table:
|
|
table = db.define_table(
|
|
tablename,
|
|
db.Field('ticket_id', length=100),
|
|
db.Field('ticket_data', 'text'),
|
|
db.Field('created_datetime', 'datetime'))
|
|
return table
|
|
|
|
def load(
|
|
self,
|
|
request,
|
|
app,
|
|
ticket_id,
|
|
):
|
|
if not self.db:
|
|
try:
|
|
ef = self._error_file(request, ticket_id, 'rb', app)
|
|
except IOError:
|
|
return {}
|
|
try:
|
|
return pickle.load(ef)
|
|
finally:
|
|
ef.close()
|
|
else:
|
|
table = self._get_table(self.db, self.tablename, app)
|
|
rows = self.db(table.ticket_id == ticket_id).select()
|
|
return pickle.loads(rows[0].ticket_data) if rows else {}
|
|
|
|
|
|
class RestrictedError(Exception):
|
|
"""
|
|
Class used to wrap an exception that occurs in the restricted environment
|
|
below. The traceback is used to log the exception and generate a ticket.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
layer='',
|
|
code='',
|
|
output='',
|
|
environment=None,
|
|
):
|
|
"""
|
|
Layer here is some description of where in the system the exception
|
|
occurred.
|
|
"""
|
|
if environment is None:
|
|
environment = {}
|
|
self.layer = layer
|
|
self.code = code
|
|
self.output = output
|
|
self.environment = environment
|
|
if layer:
|
|
try:
|
|
try:
|
|
self.traceback = traceback.format_exc()
|
|
except:
|
|
self.traceback = traceback.format_exc(limit=1)
|
|
except:
|
|
self.traceback = 'no traceback because template parsing error'
|
|
try:
|
|
self.snapshot = snapshot(context=10, code=code,
|
|
environment=self.environment)
|
|
except:
|
|
self.snapshot = {}
|
|
else:
|
|
self.traceback = '(no error)'
|
|
self.snapshot = {}
|
|
|
|
def log(self, request):
|
|
"""
|
|
Logs the exception.
|
|
"""
|
|
try:
|
|
d = {
|
|
'layer': str(self.layer),
|
|
'code': str(self.code),
|
|
'output': str(self.output),
|
|
'traceback': str(self.traceback),
|
|
'snapshot': self.snapshot,
|
|
}
|
|
ticket_storage = TicketStorage(db=request.tickets_db)
|
|
ticket_storage.store(request, request.uuid.split('/', 1)[1], d)
|
|
cmd_opts = global_settings.cmd_options
|
|
if cmd_opts and cmd_opts.print_errors:
|
|
logger.error(self.traceback)
|
|
return request.uuid
|
|
except:
|
|
logger.error(self.traceback)
|
|
return None
|
|
|
|
|
|
def load(self, request, app, ticket_id):
|
|
"""
|
|
Loads a logged exception.
|
|
"""
|
|
ticket_storage = TicketStorage(db=request.tickets_db)
|
|
d = ticket_storage.load(request, app, ticket_id)
|
|
|
|
self.layer = d.get('layer')
|
|
self.code = d.get('code')
|
|
self.output = d.get('output')
|
|
self.traceback = d.get('traceback')
|
|
self.snapshot = d.get('snapshot')
|
|
|
|
def __str__(self):
|
|
# safely show an useful message to the user
|
|
try:
|
|
output = self.output
|
|
if not isinstance(output, str, bytes, bytearray):
|
|
output = str(output)
|
|
if isinstance(output, unicodeT):
|
|
output = to_bytes(output)
|
|
except:
|
|
output = ""
|
|
return output
|
|
|
|
|
|
def compile2(code, layer):
|
|
return compile(code, layer, 'exec')
|
|
|
|
|
|
def restricted(ccode, environment=None, layer='Unknown', scode=None):
|
|
"""
|
|
Runs code in environment and returns the output. If an exception occurs
|
|
in code it raises a RestrictedError containing the traceback. Layer is
|
|
passed to RestrictedError to identify where the error occurred.
|
|
"""
|
|
if environment is None:
|
|
environment = {}
|
|
environment['__file__'] = layer
|
|
environment['__name__'] = '__restricted__'
|
|
try:
|
|
exec(ccode, environment)
|
|
except HTTP:
|
|
raise
|
|
except RestrictedError:
|
|
# do not encapsulate (obfuscate) the original RestrictedError
|
|
raise
|
|
except Exception as error:
|
|
# extract the exception type and value (used as output message)
|
|
etype, evalue, tb = sys.exc_info()
|
|
# XXX Show exception in Wing IDE if running in debugger
|
|
if __debug__ and 'WINGDB_ACTIVE' in os.environ:
|
|
sys.excepthook(etype, evalue, tb)
|
|
del tb
|
|
output = "%s %s" % (etype, evalue)
|
|
# Save source code in ticket when available
|
|
scode = scode if scode else ccode
|
|
raise RestrictedError(layer, scode, output, environment)
|
|
|
|
|
|
def snapshot(info=None, context=5, code=None, environment=None):
|
|
"""Return a dict describing a given traceback (based on cgitb.text)."""
|
|
import time
|
|
import linecache
|
|
import inspect
|
|
import pydoc
|
|
import cgitb
|
|
|
|
# if no exception info given, get current:
|
|
etype, evalue, etb = info or sys.exc_info()
|
|
|
|
if isinstance(etype, ClassType):
|
|
etype = etype.__name__
|
|
|
|
# create a snapshot dict with some basic information
|
|
s = {}
|
|
s['pyver'] = 'Python ' + sys.version.split()[0] + ': ' + sys.executable + ' (prefix: %s)' % sys.prefix
|
|
s['date'] = time.ctime(time.time())
|
|
|
|
# start to process frames
|
|
records = inspect.getinnerframes(etb, context)
|
|
del etb # Prevent circular references that would cause memory leaks
|
|
s['frames'] = []
|
|
for frame, file, lnum, func, lines, index in records:
|
|
file = file and os.path.abspath(file) or '?'
|
|
args, varargs, varkw, locals = inspect.getargvalues(frame)
|
|
call = ''
|
|
if func != '?':
|
|
call = inspect.formatargvalues(args, varargs, varkw, locals,
|
|
formatvalue=lambda value: '=' + pydoc.text.repr(value))
|
|
|
|
# basic frame information
|
|
f = {'file': file, 'func': func, 'call': call, 'lines': {},
|
|
'lnum': lnum}
|
|
|
|
highlight = {}
|
|
|
|
def reader(lnum=[lnum]):
|
|
highlight[lnum[0]] = 1
|
|
try:
|
|
return linecache.getline(file, lnum[0])
|
|
finally:
|
|
lnum[0] += 1
|
|
vars = cgitb.scanvars(reader, frame, locals)
|
|
|
|
# if it is a view, replace with generated code
|
|
if file.endswith('html'):
|
|
lmin = lnum > context and (lnum - context) or 0
|
|
lmax = lnum + context
|
|
lines = code.split("\n")[lmin:lmax]
|
|
index = min(context, lnum) - 1
|
|
|
|
if index is not None:
|
|
i = lnum - index
|
|
for line in lines:
|
|
f['lines'][i] = line.rstrip()
|
|
i += 1
|
|
|
|
# dump local variables (referenced in current line only)
|
|
f['dump'] = {}
|
|
for name, where, value in vars:
|
|
if name in f['dump']:
|
|
continue
|
|
if value is not cgitb.__UNDEF__:
|
|
if where == 'global':
|
|
name = 'global ' + name
|
|
elif where != 'local':
|
|
name = where + name.split('.')[-1]
|
|
f['dump'][name] = pydoc.text.repr(value)
|
|
else:
|
|
f['dump'][name] = 'undefined'
|
|
|
|
s['frames'].append(f)
|
|
|
|
# add exception type, value and attributes
|
|
s['etype'] = str(etype)
|
|
s['evalue'] = str(evalue)
|
|
s['exception'] = {}
|
|
if isinstance(evalue, BaseException):
|
|
for name in dir(evalue):
|
|
value = pydoc.text.repr(getattr(evalue, name))
|
|
s['exception'][name] = value
|
|
|
|
# add all local values (of last frame) to the snapshot
|
|
s['locals'] = {}
|
|
for name, value in locals.items():
|
|
s['locals'][name] = pydoc.text.repr(value)
|
|
|
|
# add web2py environment variables
|
|
for k, v in environment.items():
|
|
if k in ('request', 'response', 'session'):
|
|
s[k] = XML(str(BEAUTIFY(v)))
|
|
|
|
return s
|