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