1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | """ |
---|
4 | | This file is part of the web2py Web Framework |
---|
5 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
6 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
7 | |
---|
8 | Restricted environment to execute application's code |
---|
9 | ----------------------------------------------------- |
---|
10 | """ |
---|
11 | |
---|
12 | import sys |
---|
13 | from gluon._compat import pickle, ClassType, unicodeT, to_bytes |
---|
14 | import traceback |
---|
15 | import types |
---|
16 | import os |
---|
17 | import logging |
---|
18 | |
---|
19 | from gluon.storage import Storage |
---|
20 | from gluon.http import HTTP |
---|
21 | from gluon.html import BEAUTIFY, XML |
---|
22 | from gluon.settings import global_settings |
---|
23 | |
---|
24 | logger = logging.getLogger("web2py") |
---|
25 | |
---|
26 | __all__ = ['RestrictedError', 'restricted', 'TicketStorage', 'compile2'] |
---|
27 | |
---|
28 | |
---|
29 | class TicketStorage(Storage): |
---|
30 | |
---|
31 | """ |
---|
32 | Defines the ticket object and the default values of its members (None) |
---|
33 | """ |
---|
34 | |
---|
35 | def __init__( |
---|
36 | self, |
---|
37 | db=None, |
---|
38 | tablename='web2py_ticket' |
---|
39 | ): |
---|
40 | Storage.__init__(self) |
---|
41 | self.db = db |
---|
42 | self.tablename = tablename |
---|
43 | |
---|
44 | def store(self, request, ticket_id, ticket_data): |
---|
45 | """ |
---|
46 | Stores the ticket. It will figure out if this must be on disk or in db |
---|
47 | """ |
---|
48 | if self.db: |
---|
49 | self._store_in_db(request, ticket_id, ticket_data) |
---|
50 | else: |
---|
51 | self._store_on_disk(request, ticket_id, ticket_data) |
---|
52 | |
---|
53 | def _store_in_db(self, request, ticket_id, ticket_data): |
---|
54 | self.db._adapter.reconnect() |
---|
55 | try: |
---|
56 | table = self._get_table(self.db, self.tablename, request.application) |
---|
57 | table.insert(ticket_id=ticket_id, |
---|
58 | ticket_data=pickle.dumps(ticket_data, pickle.HIGHEST_PROTOCOL), |
---|
59 | created_datetime=request.now) |
---|
60 | self.db.commit() |
---|
61 | message = 'In FILE: %(layer)s\n\n%(traceback)s\n' |
---|
62 | except Exception: |
---|
63 | self.db.rollback() |
---|
64 | message =' Unable to store in FILE: %(layer)s\n\n%(traceback)s\n' |
---|
65 | self.db.close() |
---|
66 | logger.error(message % ticket_data) |
---|
67 | |
---|
68 | def _store_on_disk(self, request, ticket_id, ticket_data): |
---|
69 | ef = self._error_file(request, ticket_id, 'wb') |
---|
70 | try: |
---|
71 | pickle.dump(ticket_data, ef) |
---|
72 | finally: |
---|
73 | ef.close() |
---|
74 | |
---|
75 | def _error_file(self, request, ticket_id, mode, app=None): |
---|
76 | root = request.folder |
---|
77 | if app: |
---|
78 | root = os.path.join(os.path.join(root, '..'), app) |
---|
79 | errors_folder = os.path.abspath( |
---|
80 | os.path.join(root, 'errors')) # .replace('\\', '/') |
---|
81 | return open(os.path.join(errors_folder, ticket_id), mode) |
---|
82 | |
---|
83 | def _get_table(self, db, tablename, app): |
---|
84 | tablename = tablename + '_' + app |
---|
85 | table = db.get(tablename) |
---|
86 | if not table: |
---|
87 | table = db.define_table( |
---|
88 | tablename, |
---|
89 | db.Field('ticket_id', length=100), |
---|
90 | db.Field('ticket_data', 'text'), |
---|
91 | db.Field('created_datetime', 'datetime')) |
---|
92 | return table |
---|
93 | |
---|
94 | def load( |
---|
95 | self, |
---|
96 | request, |
---|
97 | app, |
---|
98 | ticket_id, |
---|
99 | ): |
---|
100 | if not self.db: |
---|
101 | try: |
---|
102 | ef = self._error_file(request, ticket_id, 'rb', app) |
---|
103 | except IOError: |
---|
104 | return {} |
---|
105 | try: |
---|
106 | return pickle.load(ef) |
---|
107 | finally: |
---|
108 | ef.close() |
---|
109 | else: |
---|
110 | table = self._get_table(self.db, self.tablename, app) |
---|
111 | rows = self.db(table.ticket_id == ticket_id).select() |
---|
112 | return pickle.loads(rows[0].ticket_data) if rows else {} |
---|
113 | |
---|
114 | |
---|
115 | class RestrictedError(Exception): |
---|
116 | """ |
---|
117 | Class used to wrap an exception that occurs in the restricted environment |
---|
118 | below. The traceback is used to log the exception and generate a ticket. |
---|
119 | """ |
---|
120 | |
---|
121 | def __init__( |
---|
122 | self, |
---|
123 | layer='', |
---|
124 | code='', |
---|
125 | output='', |
---|
126 | environment=None, |
---|
127 | ): |
---|
128 | """ |
---|
129 | Layer here is some description of where in the system the exception |
---|
130 | occurred. |
---|
131 | """ |
---|
132 | if environment is None: |
---|
133 | environment = {} |
---|
134 | self.layer = layer |
---|
135 | self.code = code |
---|
136 | self.output = output |
---|
137 | self.environment = environment |
---|
138 | if layer: |
---|
139 | try: |
---|
140 | try: |
---|
141 | self.traceback = traceback.format_exc() |
---|
142 | except: |
---|
143 | self.traceback = traceback.format_exc(limit=1) |
---|
144 | except: |
---|
145 | self.traceback = 'no traceback because template parsing error' |
---|
146 | try: |
---|
147 | self.snapshot = snapshot(context=10, code=code, |
---|
148 | environment=self.environment) |
---|
149 | except: |
---|
150 | self.snapshot = {} |
---|
151 | else: |
---|
152 | self.traceback = '(no error)' |
---|
153 | self.snapshot = {} |
---|
154 | |
---|
155 | def log(self, request): |
---|
156 | """ |
---|
157 | Logs the exception. |
---|
158 | """ |
---|
159 | try: |
---|
160 | d = { |
---|
161 | 'layer': str(self.layer), |
---|
162 | 'code': str(self.code), |
---|
163 | 'output': str(self.output), |
---|
164 | 'traceback': str(self.traceback), |
---|
165 | 'snapshot': self.snapshot, |
---|
166 | } |
---|
167 | ticket_storage = TicketStorage(db=request.tickets_db) |
---|
168 | ticket_storage.store(request, request.uuid.split('/', 1)[1], d) |
---|
169 | cmd_opts = global_settings.cmd_options |
---|
170 | if cmd_opts and cmd_opts.errors_to_console: |
---|
171 | logger.error(self.traceback) |
---|
172 | return request.uuid |
---|
173 | except: |
---|
174 | logger.error(self.traceback) |
---|
175 | return None |
---|
176 | |
---|
177 | |
---|
178 | def load(self, request, app, ticket_id): |
---|
179 | """ |
---|
180 | Loads a logged exception. |
---|
181 | """ |
---|
182 | ticket_storage = TicketStorage(db=request.tickets_db) |
---|
183 | d = ticket_storage.load(request, app, ticket_id) |
---|
184 | |
---|
185 | self.layer = d.get('layer') |
---|
186 | self.code = d.get('code') |
---|
187 | self.output = d.get('output') |
---|
188 | self.traceback = d.get('traceback') |
---|
189 | self.snapshot = d.get('snapshot') |
---|
190 | |
---|
191 | def __str__(self): |
---|
192 | # safely show an useful message to the user |
---|
193 | try: |
---|
194 | output = self.output |
---|
195 | if not isinstance(output, str, bytes, bytearray): |
---|
196 | output = str(output) |
---|
197 | if isinstance(output, unicodeT): |
---|
198 | output = to_bytes(output) |
---|
199 | except: |
---|
200 | output = "" |
---|
201 | return output |
---|
202 | |
---|
203 | |
---|
204 | def compile2(code, layer): |
---|
205 | return compile(code, layer, 'exec') |
---|
206 | |
---|
207 | |
---|
208 | def restricted(ccode, environment=None, layer='Unknown', scode=None): |
---|
209 | """ |
---|
210 | Runs code in environment and returns the output. If an exception occurs |
---|
211 | in code it raises a RestrictedError containing the traceback. Layer is |
---|
212 | passed to RestrictedError to identify where the error occurred. |
---|
213 | """ |
---|
214 | if environment is None: |
---|
215 | environment = {} |
---|
216 | environment['__file__'] = layer |
---|
217 | environment['__name__'] = '__restricted__' |
---|
218 | try: |
---|
219 | exec(ccode, environment) |
---|
220 | except HTTP: |
---|
221 | raise |
---|
222 | except RestrictedError: |
---|
223 | # do not encapsulate (obfuscate) the original RestrictedError |
---|
224 | raise |
---|
225 | except Exception as error: |
---|
226 | # extract the exception type and value (used as output message) |
---|
227 | etype, evalue, tb = sys.exc_info() |
---|
228 | # XXX Show exception in Wing IDE if running in debugger |
---|
229 | if __debug__ and 'WINGDB_ACTIVE' in os.environ: |
---|
230 | sys.excepthook(etype, evalue, tb) |
---|
231 | del tb |
---|
232 | output = "%s %s" % (etype, evalue) |
---|
233 | # Save source code in ticket when available |
---|
234 | scode = scode if scode else ccode |
---|
235 | raise RestrictedError(layer, scode, output, environment) |
---|
236 | |
---|
237 | |
---|
238 | def snapshot(info=None, context=5, code=None, environment=None): |
---|
239 | """Return a dict describing a given traceback (based on cgitb.text).""" |
---|
240 | import time |
---|
241 | import linecache |
---|
242 | import inspect |
---|
243 | import pydoc |
---|
244 | import cgitb |
---|
245 | |
---|
246 | # if no exception info given, get current: |
---|
247 | etype, evalue, etb = info or sys.exc_info() |
---|
248 | |
---|
249 | if isinstance(etype, ClassType): |
---|
250 | etype = etype.__name__ |
---|
251 | |
---|
252 | # create a snapshot dict with some basic information |
---|
253 | s = {} |
---|
254 | s['pyver'] = 'Python ' + sys.version.split()[0] + ': ' + sys.executable + ' (prefix: %s)' % sys.prefix |
---|
255 | s['date'] = time.ctime(time.time()) |
---|
256 | |
---|
257 | # start to process frames |
---|
258 | records = inspect.getinnerframes(etb, context) |
---|
259 | del etb # Prevent circular references that would cause memory leaks |
---|
260 | s['frames'] = [] |
---|
261 | for frame, file, lnum, func, lines, index in records: |
---|
262 | file = file and os.path.abspath(file) or '?' |
---|
263 | args, varargs, varkw, locals = inspect.getargvalues(frame) |
---|
264 | call = '' |
---|
265 | if func != '?': |
---|
266 | call = inspect.formatargvalues(args, varargs, varkw, locals, |
---|
267 | formatvalue=lambda value: '=' + pydoc.text.repr(value)) |
---|
268 | |
---|
269 | # basic frame information |
---|
270 | f = {'file': file, 'func': func, 'call': call, 'lines': {}, |
---|
271 | 'lnum': lnum} |
---|
272 | |
---|
273 | highlight = {} |
---|
274 | |
---|
275 | def reader(lnum=[lnum]): |
---|
276 | highlight[lnum[0]] = 1 |
---|
277 | try: |
---|
278 | return linecache.getline(file, lnum[0]) |
---|
279 | finally: |
---|
280 | lnum[0] += 1 |
---|
281 | vars = cgitb.scanvars(reader, frame, locals) |
---|
282 | |
---|
283 | # if it is a view, replace with generated code |
---|
284 | if file.endswith('html'): |
---|
285 | lmin = lnum > context and (lnum - context) or 0 |
---|
286 | lmax = lnum + context |
---|
287 | lines = code.split("\n")[lmin:lmax] |
---|
288 | index = min(context, lnum) - 1 |
---|
289 | |
---|
290 | if index is not None: |
---|
291 | i = lnum - index |
---|
292 | for line in lines: |
---|
293 | f['lines'][i] = line.rstrip() |
---|
294 | i += 1 |
---|
295 | |
---|
296 | # dump local variables (referenced in current line only) |
---|
297 | f['dump'] = {} |
---|
298 | for name, where, value in vars: |
---|
299 | if name in f['dump']: |
---|
300 | continue |
---|
301 | if value is not cgitb.__UNDEF__: |
---|
302 | if where == 'global': |
---|
303 | name = 'global ' + name |
---|
304 | elif where != 'local': |
---|
305 | name = where + name.split('.')[-1] |
---|
306 | f['dump'][name] = pydoc.text.repr(value) |
---|
307 | else: |
---|
308 | f['dump'][name] = 'undefined' |
---|
309 | |
---|
310 | s['frames'].append(f) |
---|
311 | |
---|
312 | # add exception type, value and attributes |
---|
313 | s['etype'] = str(etype) |
---|
314 | s['evalue'] = str(evalue) |
---|
315 | s['exception'] = {} |
---|
316 | if isinstance(evalue, BaseException): |
---|
317 | for name in dir(evalue): |
---|
318 | value = pydoc.text.repr(getattr(evalue, name)) |
---|
319 | s['exception'][name] = value |
---|
320 | |
---|
321 | # add all local values (of last frame) to the snapshot |
---|
322 | s['locals'] = {} |
---|
323 | for name, value in locals.items(): |
---|
324 | s['locals'][name] = pydoc.text.repr(value) |
---|
325 | |
---|
326 | # add web2py environment variables |
---|
327 | for k, v in environment.items(): |
---|
328 | if k in ('request', 'response', 'session'): |
---|
329 | s[k] = XML(str(BEAUTIFY(v))) |
---|
330 | |
---|
331 | return s |
---|