OpenSecurity/install/web.py-0.37/web/application.py
author om
Mon, 02 Dec 2013 14:02:05 +0100
changeset 3 65432e6c6042
permissions -rwxr-xr-x
initial deployment and project layout commit
     1 """
     2 Web application
     3 (from web.py)
     4 """
     5 import webapi as web
     6 import webapi, wsgi, utils
     7 import debugerror
     8 import httpserver
     9 
    10 from utils import lstrips, safeunicode
    11 import sys
    12 
    13 import urllib
    14 import traceback
    15 import itertools
    16 import os
    17 import types
    18 from exceptions import SystemExit
    19 
    20 try:
    21     import wsgiref.handlers
    22 except ImportError:
    23     pass # don't break people with old Pythons
    24 
    25 __all__ = [
    26     "application", "auto_application",
    27     "subdir_application", "subdomain_application", 
    28     "loadhook", "unloadhook",
    29     "autodelegate"
    30 ]
    31 
    32 class application:
    33     """
    34     Application to delegate requests based on path.
    35     
    36         >>> urls = ("/hello", "hello")
    37         >>> app = application(urls, globals())
    38         >>> class hello:
    39         ...     def GET(self): return "hello"
    40         >>>
    41         >>> app.request("/hello").data
    42         'hello'
    43     """
    44     def __init__(self, mapping=(), fvars={}, autoreload=None):
    45         if autoreload is None:
    46             autoreload = web.config.get('debug', False)
    47         self.init_mapping(mapping)
    48         self.fvars = fvars
    49         self.processors = []
    50         
    51         self.add_processor(loadhook(self._load))
    52         self.add_processor(unloadhook(self._unload))
    53         
    54         if autoreload:
    55             def main_module_name():
    56                 mod = sys.modules['__main__']
    57                 file = getattr(mod, '__file__', None) # make sure this works even from python interpreter
    58                 return file and os.path.splitext(os.path.basename(file))[0]
    59 
    60             def modname(fvars):
    61                 """find name of the module name from fvars."""
    62                 file, name = fvars.get('__file__'), fvars.get('__name__')
    63                 if file is None or name is None:
    64                     return None
    65 
    66                 if name == '__main__':
    67                     # Since the __main__ module can't be reloaded, the module has 
    68                     # to be imported using its file name.                    
    69                     name = main_module_name()
    70                 return name
    71                 
    72             mapping_name = utils.dictfind(fvars, mapping)
    73             module_name = modname(fvars)
    74             
    75             def reload_mapping():
    76                 """loadhook to reload mapping and fvars."""
    77                 mod = __import__(module_name, None, None, [''])
    78                 mapping = getattr(mod, mapping_name, None)
    79                 if mapping:
    80                     self.fvars = mod.__dict__
    81                     self.init_mapping(mapping)
    82 
    83             self.add_processor(loadhook(Reloader()))
    84             if mapping_name and module_name:
    85                 self.add_processor(loadhook(reload_mapping))
    86 
    87             # load __main__ module usings its filename, so that it can be reloaded.
    88             if main_module_name() and '__main__' in sys.argv:
    89                 try:
    90                     __import__(main_module_name())
    91                 except ImportError:
    92                     pass
    93                     
    94     def _load(self):
    95         web.ctx.app_stack.append(self)
    96         
    97     def _unload(self):
    98         web.ctx.app_stack = web.ctx.app_stack[:-1]
    99         
   100         if web.ctx.app_stack:
   101             # this is a sub-application, revert ctx to earlier state.
   102             oldctx = web.ctx.get('_oldctx')
   103             if oldctx:
   104                 web.ctx.home = oldctx.home
   105                 web.ctx.homepath = oldctx.homepath
   106                 web.ctx.path = oldctx.path
   107                 web.ctx.fullpath = oldctx.fullpath
   108                 
   109     def _cleanup(self):
   110         # Threads can be recycled by WSGI servers.
   111         # Clearing up all thread-local state to avoid interefereing with subsequent requests.
   112         utils.ThreadedDict.clear_all()
   113 
   114     def init_mapping(self, mapping):
   115         self.mapping = list(utils.group(mapping, 2))
   116 
   117     def add_mapping(self, pattern, classname):
   118         self.mapping.append((pattern, classname))
   119 
   120     def add_processor(self, processor):
   121         """
   122         Adds a processor to the application. 
   123         
   124             >>> urls = ("/(.*)", "echo")
   125             >>> app = application(urls, globals())
   126             >>> class echo:
   127             ...     def GET(self, name): return name
   128             ...
   129             >>>
   130             >>> def hello(handler): return "hello, " +  handler()
   131             ...
   132             >>> app.add_processor(hello)
   133             >>> app.request("/web.py").data
   134             'hello, web.py'
   135         """
   136         self.processors.append(processor)
   137 
   138     def request(self, localpart='/', method='GET', data=None,
   139                 host="0.0.0.0:8080", headers=None, https=False, **kw):
   140         """Makes request to this application for the specified path and method.
   141         Response will be a storage object with data, status and headers.
   142 
   143             >>> urls = ("/hello", "hello")
   144             >>> app = application(urls, globals())
   145             >>> class hello:
   146             ...     def GET(self): 
   147             ...         web.header('Content-Type', 'text/plain')
   148             ...         return "hello"
   149             ...
   150             >>> response = app.request("/hello")
   151             >>> response.data
   152             'hello'
   153             >>> response.status
   154             '200 OK'
   155             >>> response.headers['Content-Type']
   156             'text/plain'
   157 
   158         To use https, use https=True.
   159 
   160             >>> urls = ("/redirect", "redirect")
   161             >>> app = application(urls, globals())
   162             >>> class redirect:
   163             ...     def GET(self): raise web.seeother("/foo")
   164             ...
   165             >>> response = app.request("/redirect")
   166             >>> response.headers['Location']
   167             'http://0.0.0.0:8080/foo'
   168             >>> response = app.request("/redirect", https=True)
   169             >>> response.headers['Location']
   170             'https://0.0.0.0:8080/foo'
   171 
   172         The headers argument specifies HTTP headers as a mapping object
   173         such as a dict.
   174 
   175             >>> urls = ('/ua', 'uaprinter')
   176             >>> class uaprinter:
   177             ...     def GET(self):
   178             ...         return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
   179             ... 
   180             >>> app = application(urls, globals())
   181             >>> app.request('/ua', headers = {
   182             ...      'User-Agent': 'a small jumping bean/1.0 (compatible)'
   183             ... }).data
   184             'your user-agent is a small jumping bean/1.0 (compatible)'
   185 
   186         """
   187         path, maybe_query = urllib.splitquery(localpart)
   188         query = maybe_query or ""
   189         
   190         if 'env' in kw:
   191             env = kw['env']
   192         else:
   193             env = {}
   194         env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
   195         headers = headers or {}
   196 
   197         for k, v in headers.items():
   198             env['HTTP_' + k.upper().replace('-', '_')] = v
   199 
   200         if 'HTTP_CONTENT_LENGTH' in env:
   201             env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
   202 
   203         if 'HTTP_CONTENT_TYPE' in env:
   204             env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
   205 
   206         if method not in ["HEAD", "GET"]:
   207             data = data or ''
   208             import StringIO
   209             if isinstance(data, dict):
   210                 q = urllib.urlencode(data)
   211             else:
   212                 q = data
   213             env['wsgi.input'] = StringIO.StringIO(q)
   214             if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
   215                 env['CONTENT_LENGTH'] = len(q)
   216         response = web.storage()
   217         def start_response(status, headers):
   218             response.status = status
   219             response.headers = dict(headers)
   220             response.header_items = headers
   221         response.data = "".join(self.wsgifunc()(env, start_response))
   222         return response
   223 
   224     def browser(self):
   225         import browser
   226         return browser.AppBrowser(self)
   227 
   228     def handle(self):
   229         fn, args = self._match(self.mapping, web.ctx.path)
   230         return self._delegate(fn, self.fvars, args)
   231         
   232     def handle_with_processors(self):
   233         def process(processors):
   234             try:
   235                 if processors:
   236                     p, processors = processors[0], processors[1:]
   237                     return p(lambda: process(processors))
   238                 else:
   239                     return self.handle()
   240             except web.HTTPError:
   241                 raise
   242             except (KeyboardInterrupt, SystemExit):
   243                 raise
   244             except:
   245                 print >> web.debug, traceback.format_exc()
   246                 raise self.internalerror()
   247         
   248         # processors must be applied in the resvere order. (??)
   249         return process(self.processors)
   250                         
   251     def wsgifunc(self, *middleware):
   252         """Returns a WSGI-compatible function for this application."""
   253         def peep(iterator):
   254             """Peeps into an iterator by doing an iteration
   255             and returns an equivalent iterator.
   256             """
   257             # wsgi requires the headers first
   258             # so we need to do an iteration
   259             # and save the result for later
   260             try:
   261                 firstchunk = iterator.next()
   262             except StopIteration:
   263                 firstchunk = ''
   264 
   265             return itertools.chain([firstchunk], iterator)    
   266                                 
   267         def is_generator(x): return x and hasattr(x, 'next')
   268         
   269         def wsgi(env, start_resp):
   270             # clear threadlocal to avoid inteference of previous requests
   271             self._cleanup()
   272 
   273             self.load(env)
   274             try:
   275                 # allow uppercase methods only
   276                 if web.ctx.method.upper() != web.ctx.method:
   277                     raise web.nomethod()
   278 
   279                 result = self.handle_with_processors()
   280                 if is_generator(result):
   281                     result = peep(result)
   282                 else:
   283                     result = [result]
   284             except web.HTTPError, e:
   285                 result = [e.data]
   286 
   287             result = web.safestr(iter(result))
   288 
   289             status, headers = web.ctx.status, web.ctx.headers
   290             start_resp(status, headers)
   291             
   292             def cleanup():
   293                 self._cleanup()
   294                 yield '' # force this function to be a generator
   295                             
   296             return itertools.chain(result, cleanup())
   297 
   298         for m in middleware: 
   299             wsgi = m(wsgi)
   300 
   301         return wsgi
   302 
   303     def run(self, *middleware):
   304         """
   305         Starts handling requests. If called in a CGI or FastCGI context, it will follow
   306         that protocol. If called from the command line, it will start an HTTP
   307         server on the port named in the first command line argument, or, if there
   308         is no argument, on port 8080.
   309         
   310         `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
   311         function.
   312         """
   313         return wsgi.runwsgi(self.wsgifunc(*middleware))
   314 
   315     def stop(self):
   316         """Stops the http server started by run.
   317         """
   318         if httpserver.server:
   319             httpserver.server.stop()
   320             httpserver.server = None
   321     
   322     def cgirun(self, *middleware):
   323         """
   324         Return a CGI handler. This is mostly useful with Google App Engine.
   325         There you can just do:
   326         
   327             main = app.cgirun()
   328         """
   329         wsgiapp = self.wsgifunc(*middleware)
   330 
   331         try:
   332             from google.appengine.ext.webapp.util import run_wsgi_app
   333             return run_wsgi_app(wsgiapp)
   334         except ImportError:
   335             # we're not running from within Google App Engine
   336             return wsgiref.handlers.CGIHandler().run(wsgiapp)
   337     
   338     def load(self, env):
   339         """Initializes ctx using env."""
   340         ctx = web.ctx
   341         ctx.clear()
   342         ctx.status = '200 OK'
   343         ctx.headers = []
   344         ctx.output = ''
   345         ctx.environ = ctx.env = env
   346         ctx.host = env.get('HTTP_HOST')
   347 
   348         if env.get('wsgi.url_scheme') in ['http', 'https']:
   349             ctx.protocol = env['wsgi.url_scheme']
   350         elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
   351             ctx.protocol = 'https'
   352         else:
   353             ctx.protocol = 'http'
   354         ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]')
   355         ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
   356         ctx.home = ctx.homedomain + ctx.homepath
   357         #@@ home is changed when the request is handled to a sub-application.
   358         #@@ but the real home is required for doing absolute redirects.
   359         ctx.realhome = ctx.home
   360         ctx.ip = env.get('REMOTE_ADDR')
   361         ctx.method = env.get('REQUEST_METHOD')
   362         ctx.path = env.get('PATH_INFO')
   363         # http://trac.lighttpd.net/trac/ticket/406 requires:
   364         if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
   365             ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath)
   366             # Apache and CherryPy webservers unquote the url but lighttpd doesn't. 
   367             # unquote explicitly for lighttpd to make ctx.path uniform across all servers.
   368             ctx.path = urllib.unquote(ctx.path)
   369 
   370         if env.get('QUERY_STRING'):
   371             ctx.query = '?' + env.get('QUERY_STRING', '')
   372         else:
   373             ctx.query = ''
   374 
   375         ctx.fullpath = ctx.path + ctx.query
   376         
   377         for k, v in ctx.iteritems():
   378             # convert all string values to unicode values and replace 
   379             # malformed data with a suitable replacement marker.
   380             if isinstance(v, str):
   381                 ctx[k] = v.decode('utf-8', 'replace') 
   382 
   383         # status must always be str
   384         ctx.status = '200 OK'
   385         
   386         ctx.app_stack = []
   387 
   388     def _delegate(self, f, fvars, args=[]):
   389         def handle_class(cls):
   390             meth = web.ctx.method
   391             if meth == 'HEAD' and not hasattr(cls, meth):
   392                 meth = 'GET'
   393             if not hasattr(cls, meth):
   394                 raise web.nomethod(cls)
   395             tocall = getattr(cls(), meth)
   396             return tocall(*args)
   397             
   398         def is_class(o): return isinstance(o, (types.ClassType, type))
   399             
   400         if f is None:
   401             raise web.notfound()
   402         elif isinstance(f, application):
   403             return f.handle_with_processors()
   404         elif is_class(f):
   405             return handle_class(f)
   406         elif isinstance(f, basestring):
   407             if f.startswith('redirect '):
   408                 url = f.split(' ', 1)[1]
   409                 if web.ctx.method == "GET":
   410                     x = web.ctx.env.get('QUERY_STRING', '')
   411                     if x:
   412                         url += '?' + x
   413                 raise web.redirect(url)
   414             elif '.' in f:
   415                 mod, cls = f.rsplit('.', 1)
   416                 mod = __import__(mod, None, None, [''])
   417                 cls = getattr(mod, cls)
   418             else:
   419                 cls = fvars[f]
   420             return handle_class(cls)
   421         elif hasattr(f, '__call__'):
   422             return f()
   423         else:
   424             return web.notfound()
   425 
   426     def _match(self, mapping, value):
   427         for pat, what in mapping:
   428             if isinstance(what, application):
   429                 if value.startswith(pat):
   430                     f = lambda: self._delegate_sub_application(pat, what)
   431                     return f, None
   432                 else:
   433                     continue
   434             elif isinstance(what, basestring):
   435                 what, result = utils.re_subm('^' + pat + '$', what, value)
   436             else:
   437                 result = utils.re_compile('^' + pat + '$').match(value)
   438                 
   439             if result: # it's a match
   440                 return what, [x for x in result.groups()]
   441         return None, None
   442         
   443     def _delegate_sub_application(self, dir, app):
   444         """Deletes request to sub application `app` rooted at the directory `dir`.
   445         The home, homepath, path and fullpath values in web.ctx are updated to mimic request
   446         to the subapp and are restored after it is handled. 
   447         
   448         @@Any issues with when used with yield?
   449         """
   450         web.ctx._oldctx = web.storage(web.ctx)
   451         web.ctx.home += dir
   452         web.ctx.homepath += dir
   453         web.ctx.path = web.ctx.path[len(dir):]
   454         web.ctx.fullpath = web.ctx.fullpath[len(dir):]
   455         return app.handle_with_processors()
   456             
   457     def get_parent_app(self):
   458         if self in web.ctx.app_stack:
   459             index = web.ctx.app_stack.index(self)
   460             if index > 0:
   461                 return web.ctx.app_stack[index-1]
   462         
   463     def notfound(self):
   464         """Returns HTTPError with '404 not found' message"""
   465         parent = self.get_parent_app()
   466         if parent:
   467             return parent.notfound()
   468         else:
   469             return web._NotFound()
   470             
   471     def internalerror(self):
   472         """Returns HTTPError with '500 internal error' message"""
   473         parent = self.get_parent_app()
   474         if parent:
   475             return parent.internalerror()
   476         elif web.config.get('debug'):
   477             import debugerror
   478             return debugerror.debugerror()
   479         else:
   480             return web._InternalError()
   481 
   482 class auto_application(application):
   483     """Application similar to `application` but urls are constructed 
   484     automatiacally using metaclass.
   485 
   486         >>> app = auto_application()
   487         >>> class hello(app.page):
   488         ...     def GET(self): return "hello, world"
   489         ...
   490         >>> class foo(app.page):
   491         ...     path = '/foo/.*'
   492         ...     def GET(self): return "foo"
   493         >>> app.request("/hello").data
   494         'hello, world'
   495         >>> app.request('/foo/bar').data
   496         'foo'
   497     """
   498     def __init__(self):
   499         application.__init__(self)
   500 
   501         class metapage(type):
   502             def __init__(klass, name, bases, attrs):
   503                 type.__init__(klass, name, bases, attrs)
   504                 path = attrs.get('path', '/' + name)
   505 
   506                 # path can be specified as None to ignore that class
   507                 # typically required to create a abstract base class.
   508                 if path is not None:
   509                     self.add_mapping(path, klass)
   510 
   511         class page:
   512             path = None
   513             __metaclass__ = metapage
   514 
   515         self.page = page
   516 
   517 # The application class already has the required functionality of subdir_application
   518 subdir_application = application
   519                 
   520 class subdomain_application(application):
   521     """
   522     Application to delegate requests based on the host.
   523 
   524         >>> urls = ("/hello", "hello")
   525         >>> app = application(urls, globals())
   526         >>> class hello:
   527         ...     def GET(self): return "hello"
   528         >>>
   529         >>> mapping = (r"hello\.example\.com", app)
   530         >>> app2 = subdomain_application(mapping)
   531         >>> app2.request("/hello", host="hello.example.com").data
   532         'hello'
   533         >>> response = app2.request("/hello", host="something.example.com")
   534         >>> response.status
   535         '404 Not Found'
   536         >>> response.data
   537         'not found'
   538     """
   539     def handle(self):
   540         host = web.ctx.host.split(':')[0] #strip port
   541         fn, args = self._match(self.mapping, host)
   542         return self._delegate(fn, self.fvars, args)
   543         
   544     def _match(self, mapping, value):
   545         for pat, what in mapping:
   546             if isinstance(what, basestring):
   547                 what, result = utils.re_subm('^' + pat + '$', what, value)
   548             else:
   549                 result = utils.re_compile('^' + pat + '$').match(value)
   550 
   551             if result: # it's a match
   552                 return what, [x for x in result.groups()]
   553         return None, None
   554         
   555 def loadhook(h):
   556     """
   557     Converts a load hook into an application processor.
   558     
   559         >>> app = auto_application()
   560         >>> def f(): "something done before handling request"
   561         ...
   562         >>> app.add_processor(loadhook(f))
   563     """
   564     def processor(handler):
   565         h()
   566         return handler()
   567         
   568     return processor
   569     
   570 def unloadhook(h):
   571     """
   572     Converts an unload hook into an application processor.
   573     
   574         >>> app = auto_application()
   575         >>> def f(): "something done after handling request"
   576         ...
   577         >>> app.add_processor(unloadhook(f))    
   578     """
   579     def processor(handler):
   580         try:
   581             result = handler()
   582             is_generator = result and hasattr(result, 'next')
   583         except:
   584             # run the hook even when handler raises some exception
   585             h()
   586             raise
   587 
   588         if is_generator:
   589             return wrap(result)
   590         else:
   591             h()
   592             return result
   593             
   594     def wrap(result):
   595         def next():
   596             try:
   597                 return result.next()
   598             except:
   599                 # call the hook at the and of iterator
   600                 h()
   601                 raise
   602 
   603         result = iter(result)
   604         while True:
   605             yield next()
   606             
   607     return processor
   608 
   609 def autodelegate(prefix=''):
   610     """
   611     Returns a method that takes one argument and calls the method named prefix+arg,
   612     calling `notfound()` if there isn't one. Example:
   613 
   614         urls = ('/prefs/(.*)', 'prefs')
   615 
   616         class prefs:
   617             GET = autodelegate('GET_')
   618             def GET_password(self): pass
   619             def GET_privacy(self): pass
   620 
   621     `GET_password` would get called for `/prefs/password` while `GET_privacy` for 
   622     `GET_privacy` gets called for `/prefs/privacy`.
   623     
   624     If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
   625     is called.
   626     """
   627     def internal(self, arg):
   628         if '/' in arg:
   629             first, rest = arg.split('/', 1)
   630             func = prefix + first
   631             args = ['/' + rest]
   632         else:
   633             func = prefix + arg
   634             args = []
   635         
   636         if hasattr(self, func):
   637             try:
   638                 return getattr(self, func)(*args)
   639             except TypeError:
   640                 raise web.notfound()
   641         else:
   642             raise web.notfound()
   643     return internal
   644 
   645 class Reloader:
   646     """Checks to see if any loaded modules have changed on disk and, 
   647     if so, reloads them.
   648     """
   649 
   650     """File suffix of compiled modules."""
   651     if sys.platform.startswith('java'):
   652         SUFFIX = '$py.class'
   653     else:
   654         SUFFIX = '.pyc'
   655     
   656     def __init__(self):
   657         self.mtimes = {}
   658 
   659     def __call__(self):
   660         for mod in sys.modules.values():
   661             self.check(mod)
   662 
   663     def check(self, mod):
   664         # jython registers java packages as modules but they either
   665         # don't have a __file__ attribute or its value is None
   666         if not (mod and hasattr(mod, '__file__') and mod.__file__):
   667             return
   668 
   669         try: 
   670             mtime = os.stat(mod.__file__).st_mtime
   671         except (OSError, IOError):
   672             return
   673         if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exists(mod.__file__[:-1]):
   674             mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
   675             
   676         if mod not in self.mtimes:
   677             self.mtimes[mod] = mtime
   678         elif self.mtimes[mod] < mtime:
   679             try: 
   680                 reload(mod)
   681                 self.mtimes[mod] = mtime
   682             except ImportError: 
   683                 pass
   684                 
   685 if __name__ == "__main__":
   686     import doctest
   687     doctest.testmod()