OpenSecurity/install/web.py-0.37/web/webapi.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 API (wrapper around WSGI)
     3 (from web.py)
     4 """
     5 
     6 __all__ = [
     7     "config",
     8     "header", "debug",
     9     "input", "data",
    10     "setcookie", "cookies",
    11     "ctx", 
    12     "HTTPError", 
    13 
    14     # 200, 201, 202
    15     "OK", "Created", "Accepted",    
    16     "ok", "created", "accepted",
    17     
    18     # 301, 302, 303, 304, 307
    19     "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect", 
    20     "redirect", "found", "seeother", "notmodified", "tempredirect",
    21 
    22     # 400, 401, 403, 404, 405, 406, 409, 410, 412, 415
    23     "BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", "UnsupportedMediaType",
    24     "badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed", "unsupportedmediatype",
    25 
    26     # 500
    27     "InternalError", 
    28     "internalerror",
    29 ]
    30 
    31 import sys, cgi, Cookie, pprint, urlparse, urllib
    32 from utils import storage, storify, threadeddict, dictadd, intget, safestr
    33 
    34 config = storage()
    35 config.__doc__ = """
    36 A configuration object for various aspects of web.py.
    37 
    38 `debug`
    39    : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
    40 """
    41 
    42 class HTTPError(Exception):
    43     def __init__(self, status, headers={}, data=""):
    44         ctx.status = status
    45         for k, v in headers.items():
    46             header(k, v)
    47         self.data = data
    48         Exception.__init__(self, status)
    49         
    50 def _status_code(status, data=None, classname=None, docstring=None):
    51     if data is None:
    52         data = status.split(" ", 1)[1]
    53     classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified    
    54     docstring = docstring or '`%s` status' % status
    55 
    56     def __init__(self, data=data, headers={}):
    57         HTTPError.__init__(self, status, headers, data)
    58         
    59     # trick to create class dynamically with dynamic docstring.
    60     return type(classname, (HTTPError, object), {
    61         '__doc__': docstring,
    62         '__init__': __init__
    63     })
    64 
    65 ok = OK = _status_code("200 OK", data="")
    66 created = Created = _status_code("201 Created")
    67 accepted = Accepted = _status_code("202 Accepted")
    68 
    69 class Redirect(HTTPError):
    70     """A `301 Moved Permanently` redirect."""
    71     def __init__(self, url, status='301 Moved Permanently', absolute=False):
    72         """
    73         Returns a `status` redirect to the new URL. 
    74         `url` is joined with the base URL so that things like 
    75         `redirect("about") will work properly.
    76         """
    77         newloc = urlparse.urljoin(ctx.path, url)
    78 
    79         if newloc.startswith('/'):
    80             if absolute:
    81                 home = ctx.realhome
    82             else:
    83                 home = ctx.home
    84             newloc = home + newloc
    85 
    86         headers = {
    87             'Content-Type': 'text/html',
    88             'Location': newloc
    89         }
    90         HTTPError.__init__(self, status, headers, "")
    91 
    92 redirect = Redirect
    93 
    94 class Found(Redirect):
    95     """A `302 Found` redirect."""
    96     def __init__(self, url, absolute=False):
    97         Redirect.__init__(self, url, '302 Found', absolute=absolute)
    98 
    99 found = Found
   100 
   101 class SeeOther(Redirect):
   102     """A `303 See Other` redirect."""
   103     def __init__(self, url, absolute=False):
   104         Redirect.__init__(self, url, '303 See Other', absolute=absolute)
   105     
   106 seeother = SeeOther
   107 
   108 class NotModified(HTTPError):
   109     """A `304 Not Modified` status."""
   110     def __init__(self):
   111         HTTPError.__init__(self, "304 Not Modified")
   112 
   113 notmodified = NotModified
   114 
   115 class TempRedirect(Redirect):
   116     """A `307 Temporary Redirect` redirect."""
   117     def __init__(self, url, absolute=False):
   118         Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
   119 
   120 tempredirect = TempRedirect
   121 
   122 class BadRequest(HTTPError):
   123     """`400 Bad Request` error."""
   124     message = "bad request"
   125     def __init__(self, message=None):
   126         status = "400 Bad Request"
   127         headers = {'Content-Type': 'text/html'}
   128         HTTPError.__init__(self, status, headers, message or self.message)
   129 
   130 badrequest = BadRequest
   131 
   132 class Unauthorized(HTTPError):
   133     """`401 Unauthorized` error."""
   134     message = "unauthorized"
   135     def __init__(self):
   136         status = "401 Unauthorized"
   137         headers = {'Content-Type': 'text/html'}
   138         HTTPError.__init__(self, status, headers, self.message)
   139 
   140 unauthorized = Unauthorized
   141 
   142 class Forbidden(HTTPError):
   143     """`403 Forbidden` error."""
   144     message = "forbidden"
   145     def __init__(self):
   146         status = "403 Forbidden"
   147         headers = {'Content-Type': 'text/html'}
   148         HTTPError.__init__(self, status, headers, self.message)
   149 
   150 forbidden = Forbidden
   151 
   152 class _NotFound(HTTPError):
   153     """`404 Not Found` error."""
   154     message = "not found"
   155     def __init__(self, message=None):
   156         status = '404 Not Found'
   157         headers = {'Content-Type': 'text/html'}
   158         HTTPError.__init__(self, status, headers, message or self.message)
   159 
   160 def NotFound(message=None):
   161     """Returns HTTPError with '404 Not Found' error from the active application.
   162     """
   163     if message:
   164         return _NotFound(message)
   165     elif ctx.get('app_stack'):
   166         return ctx.app_stack[-1].notfound()
   167     else:
   168         return _NotFound()
   169 
   170 notfound = NotFound
   171 
   172 class NoMethod(HTTPError):
   173     """A `405 Method Not Allowed` error."""
   174     def __init__(self, cls=None):
   175         status = '405 Method Not Allowed'
   176         headers = {}
   177         headers['Content-Type'] = 'text/html'
   178         
   179         methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
   180         if cls:
   181             methods = [method for method in methods if hasattr(cls, method)]
   182 
   183         headers['Allow'] = ', '.join(methods)
   184         data = None
   185         HTTPError.__init__(self, status, headers, data)
   186         
   187 nomethod = NoMethod
   188 
   189 class NotAcceptable(HTTPError):
   190     """`406 Not Acceptable` error."""
   191     message = "not acceptable"
   192     def __init__(self):
   193         status = "406 Not Acceptable"
   194         headers = {'Content-Type': 'text/html'}
   195         HTTPError.__init__(self, status, headers, self.message)
   196 
   197 notacceptable = NotAcceptable
   198 
   199 class Conflict(HTTPError):
   200     """`409 Conflict` error."""
   201     message = "conflict"
   202     def __init__(self):
   203         status = "409 Conflict"
   204         headers = {'Content-Type': 'text/html'}
   205         HTTPError.__init__(self, status, headers, self.message)
   206 
   207 conflict = Conflict
   208 
   209 class Gone(HTTPError):
   210     """`410 Gone` error."""
   211     message = "gone"
   212     def __init__(self):
   213         status = '410 Gone'
   214         headers = {'Content-Type': 'text/html'}
   215         HTTPError.__init__(self, status, headers, self.message)
   216 
   217 gone = Gone
   218 
   219 class PreconditionFailed(HTTPError):
   220     """`412 Precondition Failed` error."""
   221     message = "precondition failed"
   222     def __init__(self):
   223         status = "412 Precondition Failed"
   224         headers = {'Content-Type': 'text/html'}
   225         HTTPError.__init__(self, status, headers, self.message)
   226 
   227 preconditionfailed = PreconditionFailed
   228 
   229 class UnsupportedMediaType(HTTPError):
   230     """`415 Unsupported Media Type` error."""
   231     message = "unsupported media type"
   232     def __init__(self):
   233         status = "415 Unsupported Media Type"
   234         headers = {'Content-Type': 'text/html'}
   235         HTTPError.__init__(self, status, headers, self.message)
   236 
   237 unsupportedmediatype = UnsupportedMediaType
   238 
   239 class _InternalError(HTTPError):
   240     """500 Internal Server Error`."""
   241     message = "internal server error"
   242     
   243     def __init__(self, message=None):
   244         status = '500 Internal Server Error'
   245         headers = {'Content-Type': 'text/html'}
   246         HTTPError.__init__(self, status, headers, message or self.message)
   247 
   248 def InternalError(message=None):
   249     """Returns HTTPError with '500 internal error' error from the active application.
   250     """
   251     if message:
   252         return _InternalError(message)
   253     elif ctx.get('app_stack'):
   254         return ctx.app_stack[-1].internalerror()
   255     else:
   256         return _InternalError()
   257 
   258 internalerror = InternalError
   259 
   260 def header(hdr, value, unique=False):
   261     """
   262     Adds the header `hdr: value` with the response.
   263     
   264     If `unique` is True and a header with that name already exists,
   265     it doesn't add a new one. 
   266     """
   267     hdr, value = safestr(hdr), safestr(value)
   268     # protection against HTTP response splitting attack
   269     if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
   270         raise ValueError, 'invalid characters in header'
   271         
   272     if unique is True:
   273         for h, v in ctx.headers:
   274             if h.lower() == hdr.lower(): return
   275     
   276     ctx.headers.append((hdr, value))
   277     
   278 def rawinput(method=None):
   279     """Returns storage object with GET or POST arguments.
   280     """
   281     method = method or "both"
   282     from cStringIO import StringIO
   283 
   284     def dictify(fs): 
   285         # hack to make web.input work with enctype='text/plain.
   286         if fs.list is None:
   287             fs.list = [] 
   288 
   289         return dict([(k, fs[k]) for k in fs.keys()])
   290     
   291     e = ctx.env.copy()
   292     a = b = {}
   293     
   294     if method.lower() in ['both', 'post', 'put']:
   295         if e['REQUEST_METHOD'] in ['POST', 'PUT']:
   296             if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
   297                 # since wsgi.input is directly passed to cgi.FieldStorage, 
   298                 # it can not be called multiple times. Saving the FieldStorage
   299                 # object in ctx to allow calling web.input multiple times.
   300                 a = ctx.get('_fieldstorage')
   301                 if not a:
   302                     fp = e['wsgi.input']
   303                     a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
   304                     ctx._fieldstorage = a
   305             else:
   306                 fp = StringIO(data())
   307                 a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
   308             a = dictify(a)
   309 
   310     if method.lower() in ['both', 'get']:
   311         e['REQUEST_METHOD'] = 'GET'
   312         b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
   313 
   314     def process_fieldstorage(fs):
   315         if isinstance(fs, list):
   316             return [process_fieldstorage(x) for x in fs]
   317         elif fs.filename is None:
   318             return fs.value
   319         else:
   320             return fs
   321 
   322     return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
   323 
   324 def input(*requireds, **defaults):
   325     """
   326     Returns a `storage` object with the GET and POST arguments. 
   327     See `storify` for how `requireds` and `defaults` work.
   328     """
   329     _method = defaults.pop('_method', 'both')
   330     out = rawinput(_method)
   331     try:
   332         defaults.setdefault('_unicode', True) # force unicode conversion by default.
   333         return storify(out, *requireds, **defaults)
   334     except KeyError:
   335         raise badrequest()
   336 
   337 def data():
   338     """Returns the data sent with the request."""
   339     if 'data' not in ctx:
   340         cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
   341         ctx.data = ctx.env['wsgi.input'].read(cl)
   342     return ctx.data
   343 
   344 def setcookie(name, value, expires='', domain=None,
   345               secure=False, httponly=False, path=None):
   346     """Sets a cookie."""
   347     morsel = Cookie.Morsel()
   348     name, value = safestr(name), safestr(value)
   349     morsel.set(name, value, urllib.quote(value))
   350     if expires < 0:
   351         expires = -1000000000
   352     morsel['expires'] = expires
   353     morsel['path'] = path or ctx.homepath+'/'
   354     if domain:
   355         morsel['domain'] = domain
   356     if secure:
   357         morsel['secure'] = secure
   358     value = morsel.OutputString()
   359     if httponly:
   360         value += '; httponly'
   361     header('Set-Cookie', value)
   362         
   363 def decode_cookie(value):
   364     r"""Safely decodes a cookie value to unicode. 
   365     
   366     Tries us-ascii, utf-8 and io8859 encodings, in that order.
   367 
   368     >>> decode_cookie('')
   369     u''
   370     >>> decode_cookie('asdf')
   371     u'asdf'
   372     >>> decode_cookie('foo \xC3\xA9 bar')
   373     u'foo \xe9 bar'
   374     >>> decode_cookie('foo \xE9 bar')
   375     u'foo \xe9 bar'
   376     """
   377     try:
   378         # First try plain ASCII encoding
   379         return unicode(value, 'us-ascii')
   380     except UnicodeError:
   381         # Then try UTF-8, and if that fails, ISO8859
   382         try:
   383             return unicode(value, 'utf-8')
   384         except UnicodeError:
   385             return unicode(value, 'iso8859', 'ignore')
   386 
   387 def parse_cookies(http_cookie):
   388     r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
   389         
   390     >>> sorted(parse_cookies('').items())
   391     []
   392     >>> sorted(parse_cookies('a=1').items())
   393     [('a', '1')]
   394     >>> sorted(parse_cookies('a=1%202').items())
   395     [('a', '1 2')]
   396     >>> sorted(parse_cookies('a=Z%C3%A9Z').items())
   397     [('a', 'Z\xc3\xa9Z')]
   398     >>> sorted(parse_cookies('a=1; b=2; c=3').items())
   399     [('a', '1'), ('b', '2'), ('c', '3')]
   400     >>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
   401     [('a', '1'), ('b', 'w('), ('c', '3')]
   402     >>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
   403     [('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
   404 
   405     >>> sorted(parse_cookies('keebler=E=mc2').items())
   406     [('keebler', 'E=mc2')]
   407     >>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
   408     [('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
   409     """
   410     #print "parse_cookies"
   411     if '"' in http_cookie:
   412         # HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
   413         cookie = Cookie.SimpleCookie()
   414         try:
   415             cookie.load(http_cookie)
   416         except Cookie.CookieError:
   417             # If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
   418             # first splitting on ';' and loading each attr=value pair separately
   419             cookie = Cookie.SimpleCookie()
   420             for attr_value in http_cookie.split(';'):
   421                 try:
   422                     cookie.load(attr_value)
   423                 except Cookie.CookieError:
   424                     pass
   425         cookies = dict((k, urllib.unquote(v.value)) for k, v in cookie.iteritems())
   426     else:
   427         # HTTP_COOKIE doesn't have quotes, use fast cookie parsing
   428         cookies = {}
   429         for key_value in http_cookie.split(';'):
   430             key_value = key_value.split('=', 1)
   431             if len(key_value) == 2:
   432                 key, value = key_value
   433                 cookies[key.strip()] = urllib.unquote(value.strip())
   434     return cookies
   435 
   436 def cookies(*requireds, **defaults):
   437     r"""Returns a `storage` object with all the request cookies in it.
   438     
   439     See `storify` for how `requireds` and `defaults` work.
   440 
   441     This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
   442     the cookies it can.
   443     
   444     The values are converted to unicode if _unicode=True is passed.
   445     """
   446     # If _unicode=True is specified, use decode_cookie to convert cookie value to unicode 
   447     if defaults.get("_unicode") is True:
   448         defaults['_unicode'] = decode_cookie
   449         
   450     # parse cookie string and cache the result for next time.
   451     if '_parsed_cookies' not in ctx:
   452         http_cookie = ctx.env.get("HTTP_COOKIE", "")
   453         ctx._parsed_cookies = parse_cookies(http_cookie)
   454 
   455     try:
   456         return storify(ctx._parsed_cookies, *requireds, **defaults)
   457     except KeyError:
   458         badrequest()
   459         raise StopIteration
   460 
   461 def debug(*args):
   462     """
   463     Prints a prettyprinted version of `args` to stderr.
   464     """
   465     try: 
   466         out = ctx.environ['wsgi.errors']
   467     except: 
   468         out = sys.stderr
   469     for arg in args:
   470         print >> out, pprint.pformat(arg)
   471     return ''
   472 
   473 def _debugwrite(x):
   474     try: 
   475         out = ctx.environ['wsgi.errors']
   476     except: 
   477         out = sys.stderr
   478     out.write(x)
   479 debug.write = _debugwrite
   480 
   481 ctx = context = threadeddict()
   482 
   483 ctx.__doc__ = """
   484 A `storage` object containing various information about the request:
   485   
   486 `environ` (aka `env`)
   487    : A dictionary containing the standard WSGI environment variables.
   488 
   489 `host`
   490    : The domain (`Host` header) requested by the user.
   491 
   492 `home`
   493    : The base path for the application.
   494 
   495 `ip`
   496    : The IP address of the requester.
   497 
   498 `method`
   499    : The HTTP method used.
   500 
   501 `path`
   502    : The path request.
   503    
   504 `query`
   505    : If there are no query arguments, the empty string. Otherwise, a `?` followed
   506      by the query string.
   507 
   508 `fullpath`
   509    : The full path requested, including query arguments (`== path + query`).
   510 
   511 ### Response Data
   512 
   513 `status` (default: "200 OK")
   514    : The status code to be used in the response.
   515 
   516 `headers`
   517    : A list of 2-tuples to be used in the response.
   518 
   519 `output`
   520    : A string to be used as the response.
   521 """
   522 
   523 if __name__ == "__main__":
   524     import doctest
   525     doctest.testmod()