OpenSecurity/install/web.py-0.37/web/webapi.py
changeset 3 65432e6c6042
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/OpenSecurity/install/web.py-0.37/web/webapi.py	Mon Dec 02 14:02:05 2013 +0100
     1.3 @@ -0,0 +1,525 @@
     1.4 +"""
     1.5 +Web API (wrapper around WSGI)
     1.6 +(from web.py)
     1.7 +"""
     1.8 +
     1.9 +__all__ = [
    1.10 +    "config",
    1.11 +    "header", "debug",
    1.12 +    "input", "data",
    1.13 +    "setcookie", "cookies",
    1.14 +    "ctx", 
    1.15 +    "HTTPError", 
    1.16 +
    1.17 +    # 200, 201, 202
    1.18 +    "OK", "Created", "Accepted",    
    1.19 +    "ok", "created", "accepted",
    1.20 +    
    1.21 +    # 301, 302, 303, 304, 307
    1.22 +    "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect", 
    1.23 +    "redirect", "found", "seeother", "notmodified", "tempredirect",
    1.24 +
    1.25 +    # 400, 401, 403, 404, 405, 406, 409, 410, 412, 415
    1.26 +    "BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", "UnsupportedMediaType",
    1.27 +    "badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed", "unsupportedmediatype",
    1.28 +
    1.29 +    # 500
    1.30 +    "InternalError", 
    1.31 +    "internalerror",
    1.32 +]
    1.33 +
    1.34 +import sys, cgi, Cookie, pprint, urlparse, urllib
    1.35 +from utils import storage, storify, threadeddict, dictadd, intget, safestr
    1.36 +
    1.37 +config = storage()
    1.38 +config.__doc__ = """
    1.39 +A configuration object for various aspects of web.py.
    1.40 +
    1.41 +`debug`
    1.42 +   : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
    1.43 +"""
    1.44 +
    1.45 +class HTTPError(Exception):
    1.46 +    def __init__(self, status, headers={}, data=""):
    1.47 +        ctx.status = status
    1.48 +        for k, v in headers.items():
    1.49 +            header(k, v)
    1.50 +        self.data = data
    1.51 +        Exception.__init__(self, status)
    1.52 +        
    1.53 +def _status_code(status, data=None, classname=None, docstring=None):
    1.54 +    if data is None:
    1.55 +        data = status.split(" ", 1)[1]
    1.56 +    classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified    
    1.57 +    docstring = docstring or '`%s` status' % status
    1.58 +
    1.59 +    def __init__(self, data=data, headers={}):
    1.60 +        HTTPError.__init__(self, status, headers, data)
    1.61 +        
    1.62 +    # trick to create class dynamically with dynamic docstring.
    1.63 +    return type(classname, (HTTPError, object), {
    1.64 +        '__doc__': docstring,
    1.65 +        '__init__': __init__
    1.66 +    })
    1.67 +
    1.68 +ok = OK = _status_code("200 OK", data="")
    1.69 +created = Created = _status_code("201 Created")
    1.70 +accepted = Accepted = _status_code("202 Accepted")
    1.71 +
    1.72 +class Redirect(HTTPError):
    1.73 +    """A `301 Moved Permanently` redirect."""
    1.74 +    def __init__(self, url, status='301 Moved Permanently', absolute=False):
    1.75 +        """
    1.76 +        Returns a `status` redirect to the new URL. 
    1.77 +        `url` is joined with the base URL so that things like 
    1.78 +        `redirect("about") will work properly.
    1.79 +        """
    1.80 +        newloc = urlparse.urljoin(ctx.path, url)
    1.81 +
    1.82 +        if newloc.startswith('/'):
    1.83 +            if absolute:
    1.84 +                home = ctx.realhome
    1.85 +            else:
    1.86 +                home = ctx.home
    1.87 +            newloc = home + newloc
    1.88 +
    1.89 +        headers = {
    1.90 +            'Content-Type': 'text/html',
    1.91 +            'Location': newloc
    1.92 +        }
    1.93 +        HTTPError.__init__(self, status, headers, "")
    1.94 +
    1.95 +redirect = Redirect
    1.96 +
    1.97 +class Found(Redirect):
    1.98 +    """A `302 Found` redirect."""
    1.99 +    def __init__(self, url, absolute=False):
   1.100 +        Redirect.__init__(self, url, '302 Found', absolute=absolute)
   1.101 +
   1.102 +found = Found
   1.103 +
   1.104 +class SeeOther(Redirect):
   1.105 +    """A `303 See Other` redirect."""
   1.106 +    def __init__(self, url, absolute=False):
   1.107 +        Redirect.__init__(self, url, '303 See Other', absolute=absolute)
   1.108 +    
   1.109 +seeother = SeeOther
   1.110 +
   1.111 +class NotModified(HTTPError):
   1.112 +    """A `304 Not Modified` status."""
   1.113 +    def __init__(self):
   1.114 +        HTTPError.__init__(self, "304 Not Modified")
   1.115 +
   1.116 +notmodified = NotModified
   1.117 +
   1.118 +class TempRedirect(Redirect):
   1.119 +    """A `307 Temporary Redirect` redirect."""
   1.120 +    def __init__(self, url, absolute=False):
   1.121 +        Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
   1.122 +
   1.123 +tempredirect = TempRedirect
   1.124 +
   1.125 +class BadRequest(HTTPError):
   1.126 +    """`400 Bad Request` error."""
   1.127 +    message = "bad request"
   1.128 +    def __init__(self, message=None):
   1.129 +        status = "400 Bad Request"
   1.130 +        headers = {'Content-Type': 'text/html'}
   1.131 +        HTTPError.__init__(self, status, headers, message or self.message)
   1.132 +
   1.133 +badrequest = BadRequest
   1.134 +
   1.135 +class Unauthorized(HTTPError):
   1.136 +    """`401 Unauthorized` error."""
   1.137 +    message = "unauthorized"
   1.138 +    def __init__(self):
   1.139 +        status = "401 Unauthorized"
   1.140 +        headers = {'Content-Type': 'text/html'}
   1.141 +        HTTPError.__init__(self, status, headers, self.message)
   1.142 +
   1.143 +unauthorized = Unauthorized
   1.144 +
   1.145 +class Forbidden(HTTPError):
   1.146 +    """`403 Forbidden` error."""
   1.147 +    message = "forbidden"
   1.148 +    def __init__(self):
   1.149 +        status = "403 Forbidden"
   1.150 +        headers = {'Content-Type': 'text/html'}
   1.151 +        HTTPError.__init__(self, status, headers, self.message)
   1.152 +
   1.153 +forbidden = Forbidden
   1.154 +
   1.155 +class _NotFound(HTTPError):
   1.156 +    """`404 Not Found` error."""
   1.157 +    message = "not found"
   1.158 +    def __init__(self, message=None):
   1.159 +        status = '404 Not Found'
   1.160 +        headers = {'Content-Type': 'text/html'}
   1.161 +        HTTPError.__init__(self, status, headers, message or self.message)
   1.162 +
   1.163 +def NotFound(message=None):
   1.164 +    """Returns HTTPError with '404 Not Found' error from the active application.
   1.165 +    """
   1.166 +    if message:
   1.167 +        return _NotFound(message)
   1.168 +    elif ctx.get('app_stack'):
   1.169 +        return ctx.app_stack[-1].notfound()
   1.170 +    else:
   1.171 +        return _NotFound()
   1.172 +
   1.173 +notfound = NotFound
   1.174 +
   1.175 +class NoMethod(HTTPError):
   1.176 +    """A `405 Method Not Allowed` error."""
   1.177 +    def __init__(self, cls=None):
   1.178 +        status = '405 Method Not Allowed'
   1.179 +        headers = {}
   1.180 +        headers['Content-Type'] = 'text/html'
   1.181 +        
   1.182 +        methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
   1.183 +        if cls:
   1.184 +            methods = [method for method in methods if hasattr(cls, method)]
   1.185 +
   1.186 +        headers['Allow'] = ', '.join(methods)
   1.187 +        data = None
   1.188 +        HTTPError.__init__(self, status, headers, data)
   1.189 +        
   1.190 +nomethod = NoMethod
   1.191 +
   1.192 +class NotAcceptable(HTTPError):
   1.193 +    """`406 Not Acceptable` error."""
   1.194 +    message = "not acceptable"
   1.195 +    def __init__(self):
   1.196 +        status = "406 Not Acceptable"
   1.197 +        headers = {'Content-Type': 'text/html'}
   1.198 +        HTTPError.__init__(self, status, headers, self.message)
   1.199 +
   1.200 +notacceptable = NotAcceptable
   1.201 +
   1.202 +class Conflict(HTTPError):
   1.203 +    """`409 Conflict` error."""
   1.204 +    message = "conflict"
   1.205 +    def __init__(self):
   1.206 +        status = "409 Conflict"
   1.207 +        headers = {'Content-Type': 'text/html'}
   1.208 +        HTTPError.__init__(self, status, headers, self.message)
   1.209 +
   1.210 +conflict = Conflict
   1.211 +
   1.212 +class Gone(HTTPError):
   1.213 +    """`410 Gone` error."""
   1.214 +    message = "gone"
   1.215 +    def __init__(self):
   1.216 +        status = '410 Gone'
   1.217 +        headers = {'Content-Type': 'text/html'}
   1.218 +        HTTPError.__init__(self, status, headers, self.message)
   1.219 +
   1.220 +gone = Gone
   1.221 +
   1.222 +class PreconditionFailed(HTTPError):
   1.223 +    """`412 Precondition Failed` error."""
   1.224 +    message = "precondition failed"
   1.225 +    def __init__(self):
   1.226 +        status = "412 Precondition Failed"
   1.227 +        headers = {'Content-Type': 'text/html'}
   1.228 +        HTTPError.__init__(self, status, headers, self.message)
   1.229 +
   1.230 +preconditionfailed = PreconditionFailed
   1.231 +
   1.232 +class UnsupportedMediaType(HTTPError):
   1.233 +    """`415 Unsupported Media Type` error."""
   1.234 +    message = "unsupported media type"
   1.235 +    def __init__(self):
   1.236 +        status = "415 Unsupported Media Type"
   1.237 +        headers = {'Content-Type': 'text/html'}
   1.238 +        HTTPError.__init__(self, status, headers, self.message)
   1.239 +
   1.240 +unsupportedmediatype = UnsupportedMediaType
   1.241 +
   1.242 +class _InternalError(HTTPError):
   1.243 +    """500 Internal Server Error`."""
   1.244 +    message = "internal server error"
   1.245 +    
   1.246 +    def __init__(self, message=None):
   1.247 +        status = '500 Internal Server Error'
   1.248 +        headers = {'Content-Type': 'text/html'}
   1.249 +        HTTPError.__init__(self, status, headers, message or self.message)
   1.250 +
   1.251 +def InternalError(message=None):
   1.252 +    """Returns HTTPError with '500 internal error' error from the active application.
   1.253 +    """
   1.254 +    if message:
   1.255 +        return _InternalError(message)
   1.256 +    elif ctx.get('app_stack'):
   1.257 +        return ctx.app_stack[-1].internalerror()
   1.258 +    else:
   1.259 +        return _InternalError()
   1.260 +
   1.261 +internalerror = InternalError
   1.262 +
   1.263 +def header(hdr, value, unique=False):
   1.264 +    """
   1.265 +    Adds the header `hdr: value` with the response.
   1.266 +    
   1.267 +    If `unique` is True and a header with that name already exists,
   1.268 +    it doesn't add a new one. 
   1.269 +    """
   1.270 +    hdr, value = safestr(hdr), safestr(value)
   1.271 +    # protection against HTTP response splitting attack
   1.272 +    if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
   1.273 +        raise ValueError, 'invalid characters in header'
   1.274 +        
   1.275 +    if unique is True:
   1.276 +        for h, v in ctx.headers:
   1.277 +            if h.lower() == hdr.lower(): return
   1.278 +    
   1.279 +    ctx.headers.append((hdr, value))
   1.280 +    
   1.281 +def rawinput(method=None):
   1.282 +    """Returns storage object with GET or POST arguments.
   1.283 +    """
   1.284 +    method = method or "both"
   1.285 +    from cStringIO import StringIO
   1.286 +
   1.287 +    def dictify(fs): 
   1.288 +        # hack to make web.input work with enctype='text/plain.
   1.289 +        if fs.list is None:
   1.290 +            fs.list = [] 
   1.291 +
   1.292 +        return dict([(k, fs[k]) for k in fs.keys()])
   1.293 +    
   1.294 +    e = ctx.env.copy()
   1.295 +    a = b = {}
   1.296 +    
   1.297 +    if method.lower() in ['both', 'post', 'put']:
   1.298 +        if e['REQUEST_METHOD'] in ['POST', 'PUT']:
   1.299 +            if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
   1.300 +                # since wsgi.input is directly passed to cgi.FieldStorage, 
   1.301 +                # it can not be called multiple times. Saving the FieldStorage
   1.302 +                # object in ctx to allow calling web.input multiple times.
   1.303 +                a = ctx.get('_fieldstorage')
   1.304 +                if not a:
   1.305 +                    fp = e['wsgi.input']
   1.306 +                    a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
   1.307 +                    ctx._fieldstorage = a
   1.308 +            else:
   1.309 +                fp = StringIO(data())
   1.310 +                a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
   1.311 +            a = dictify(a)
   1.312 +
   1.313 +    if method.lower() in ['both', 'get']:
   1.314 +        e['REQUEST_METHOD'] = 'GET'
   1.315 +        b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
   1.316 +
   1.317 +    def process_fieldstorage(fs):
   1.318 +        if isinstance(fs, list):
   1.319 +            return [process_fieldstorage(x) for x in fs]
   1.320 +        elif fs.filename is None:
   1.321 +            return fs.value
   1.322 +        else:
   1.323 +            return fs
   1.324 +
   1.325 +    return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
   1.326 +
   1.327 +def input(*requireds, **defaults):
   1.328 +    """
   1.329 +    Returns a `storage` object with the GET and POST arguments. 
   1.330 +    See `storify` for how `requireds` and `defaults` work.
   1.331 +    """
   1.332 +    _method = defaults.pop('_method', 'both')
   1.333 +    out = rawinput(_method)
   1.334 +    try:
   1.335 +        defaults.setdefault('_unicode', True) # force unicode conversion by default.
   1.336 +        return storify(out, *requireds, **defaults)
   1.337 +    except KeyError:
   1.338 +        raise badrequest()
   1.339 +
   1.340 +def data():
   1.341 +    """Returns the data sent with the request."""
   1.342 +    if 'data' not in ctx:
   1.343 +        cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
   1.344 +        ctx.data = ctx.env['wsgi.input'].read(cl)
   1.345 +    return ctx.data
   1.346 +
   1.347 +def setcookie(name, value, expires='', domain=None,
   1.348 +              secure=False, httponly=False, path=None):
   1.349 +    """Sets a cookie."""
   1.350 +    morsel = Cookie.Morsel()
   1.351 +    name, value = safestr(name), safestr(value)
   1.352 +    morsel.set(name, value, urllib.quote(value))
   1.353 +    if expires < 0:
   1.354 +        expires = -1000000000
   1.355 +    morsel['expires'] = expires
   1.356 +    morsel['path'] = path or ctx.homepath+'/'
   1.357 +    if domain:
   1.358 +        morsel['domain'] = domain
   1.359 +    if secure:
   1.360 +        morsel['secure'] = secure
   1.361 +    value = morsel.OutputString()
   1.362 +    if httponly:
   1.363 +        value += '; httponly'
   1.364 +    header('Set-Cookie', value)
   1.365 +        
   1.366 +def decode_cookie(value):
   1.367 +    r"""Safely decodes a cookie value to unicode. 
   1.368 +    
   1.369 +    Tries us-ascii, utf-8 and io8859 encodings, in that order.
   1.370 +
   1.371 +    >>> decode_cookie('')
   1.372 +    u''
   1.373 +    >>> decode_cookie('asdf')
   1.374 +    u'asdf'
   1.375 +    >>> decode_cookie('foo \xC3\xA9 bar')
   1.376 +    u'foo \xe9 bar'
   1.377 +    >>> decode_cookie('foo \xE9 bar')
   1.378 +    u'foo \xe9 bar'
   1.379 +    """
   1.380 +    try:
   1.381 +        # First try plain ASCII encoding
   1.382 +        return unicode(value, 'us-ascii')
   1.383 +    except UnicodeError:
   1.384 +        # Then try UTF-8, and if that fails, ISO8859
   1.385 +        try:
   1.386 +            return unicode(value, 'utf-8')
   1.387 +        except UnicodeError:
   1.388 +            return unicode(value, 'iso8859', 'ignore')
   1.389 +
   1.390 +def parse_cookies(http_cookie):
   1.391 +    r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
   1.392 +        
   1.393 +    >>> sorted(parse_cookies('').items())
   1.394 +    []
   1.395 +    >>> sorted(parse_cookies('a=1').items())
   1.396 +    [('a', '1')]
   1.397 +    >>> sorted(parse_cookies('a=1%202').items())
   1.398 +    [('a', '1 2')]
   1.399 +    >>> sorted(parse_cookies('a=Z%C3%A9Z').items())
   1.400 +    [('a', 'Z\xc3\xa9Z')]
   1.401 +    >>> sorted(parse_cookies('a=1; b=2; c=3').items())
   1.402 +    [('a', '1'), ('b', '2'), ('c', '3')]
   1.403 +    >>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
   1.404 +    [('a', '1'), ('b', 'w('), ('c', '3')]
   1.405 +    >>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
   1.406 +    [('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
   1.407 +
   1.408 +    >>> sorted(parse_cookies('keebler=E=mc2').items())
   1.409 +    [('keebler', 'E=mc2')]
   1.410 +    >>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
   1.411 +    [('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
   1.412 +    """
   1.413 +    #print "parse_cookies"
   1.414 +    if '"' in http_cookie:
   1.415 +        # HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
   1.416 +        cookie = Cookie.SimpleCookie()
   1.417 +        try:
   1.418 +            cookie.load(http_cookie)
   1.419 +        except Cookie.CookieError:
   1.420 +            # If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
   1.421 +            # first splitting on ';' and loading each attr=value pair separately
   1.422 +            cookie = Cookie.SimpleCookie()
   1.423 +            for attr_value in http_cookie.split(';'):
   1.424 +                try:
   1.425 +                    cookie.load(attr_value)
   1.426 +                except Cookie.CookieError:
   1.427 +                    pass
   1.428 +        cookies = dict((k, urllib.unquote(v.value)) for k, v in cookie.iteritems())
   1.429 +    else:
   1.430 +        # HTTP_COOKIE doesn't have quotes, use fast cookie parsing
   1.431 +        cookies = {}
   1.432 +        for key_value in http_cookie.split(';'):
   1.433 +            key_value = key_value.split('=', 1)
   1.434 +            if len(key_value) == 2:
   1.435 +                key, value = key_value
   1.436 +                cookies[key.strip()] = urllib.unquote(value.strip())
   1.437 +    return cookies
   1.438 +
   1.439 +def cookies(*requireds, **defaults):
   1.440 +    r"""Returns a `storage` object with all the request cookies in it.
   1.441 +    
   1.442 +    See `storify` for how `requireds` and `defaults` work.
   1.443 +
   1.444 +    This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
   1.445 +    the cookies it can.
   1.446 +    
   1.447 +    The values are converted to unicode if _unicode=True is passed.
   1.448 +    """
   1.449 +    # If _unicode=True is specified, use decode_cookie to convert cookie value to unicode 
   1.450 +    if defaults.get("_unicode") is True:
   1.451 +        defaults['_unicode'] = decode_cookie
   1.452 +        
   1.453 +    # parse cookie string and cache the result for next time.
   1.454 +    if '_parsed_cookies' not in ctx:
   1.455 +        http_cookie = ctx.env.get("HTTP_COOKIE", "")
   1.456 +        ctx._parsed_cookies = parse_cookies(http_cookie)
   1.457 +
   1.458 +    try:
   1.459 +        return storify(ctx._parsed_cookies, *requireds, **defaults)
   1.460 +    except KeyError:
   1.461 +        badrequest()
   1.462 +        raise StopIteration
   1.463 +
   1.464 +def debug(*args):
   1.465 +    """
   1.466 +    Prints a prettyprinted version of `args` to stderr.
   1.467 +    """
   1.468 +    try: 
   1.469 +        out = ctx.environ['wsgi.errors']
   1.470 +    except: 
   1.471 +        out = sys.stderr
   1.472 +    for arg in args:
   1.473 +        print >> out, pprint.pformat(arg)
   1.474 +    return ''
   1.475 +
   1.476 +def _debugwrite(x):
   1.477 +    try: 
   1.478 +        out = ctx.environ['wsgi.errors']
   1.479 +    except: 
   1.480 +        out = sys.stderr
   1.481 +    out.write(x)
   1.482 +debug.write = _debugwrite
   1.483 +
   1.484 +ctx = context = threadeddict()
   1.485 +
   1.486 +ctx.__doc__ = """
   1.487 +A `storage` object containing various information about the request:
   1.488 +  
   1.489 +`environ` (aka `env`)
   1.490 +   : A dictionary containing the standard WSGI environment variables.
   1.491 +
   1.492 +`host`
   1.493 +   : The domain (`Host` header) requested by the user.
   1.494 +
   1.495 +`home`
   1.496 +   : The base path for the application.
   1.497 +
   1.498 +`ip`
   1.499 +   : The IP address of the requester.
   1.500 +
   1.501 +`method`
   1.502 +   : The HTTP method used.
   1.503 +
   1.504 +`path`
   1.505 +   : The path request.
   1.506 +   
   1.507 +`query`
   1.508 +   : If there are no query arguments, the empty string. Otherwise, a `?` followed
   1.509 +     by the query string.
   1.510 +
   1.511 +`fullpath`
   1.512 +   : The full path requested, including query arguments (`== path + query`).
   1.513 +
   1.514 +### Response Data
   1.515 +
   1.516 +`status` (default: "200 OK")
   1.517 +   : The status code to be used in the response.
   1.518 +
   1.519 +`headers`
   1.520 +   : A list of 2-tuples to be used in the response.
   1.521 +
   1.522 +`output`
   1.523 +   : A string to be used as the response.
   1.524 +"""
   1.525 +
   1.526 +if __name__ == "__main__":
   1.527 +    import doctest
   1.528 +    doctest.testmod()
   1.529 \ No newline at end of file