X-Git-Url: https://code.kerkeslager.com/?a=blobdiff_plain;f=src%2Ffwx%2F__init__.py;h=d26083b99ba0c873e4f034fcbd4b79c9d11a0cb8;hb=cdcd770a863322ff01363438d0739bbcc63873e3;hp=411c07761b02dd177e1919a665b35ccd6ead9cc1;hpb=1b9f0c47a6e4ef0d19c652d6476923f6e437b28c;p=fwx diff --git a/src/fwx/__init__.py b/src/fwx/__init__.py index 411c077..d26083b 100644 --- a/src/fwx/__init__.py +++ b/src/fwx/__init__.py @@ -8,6 +8,7 @@ _Request = collections.namedtuple( ( 'env', 'GET', + 'POST', 'accept', 'accept_encoding', 'accept_language', @@ -24,7 +25,10 @@ _Request = collections.namedtuple( ) class Request(_Request): - def __new__(cls, env): + def __new__(cls, method, path, env=None): + if env is None: + env = {} + errors = [] accept = env.get('HTTP_ACCEPT') @@ -32,8 +36,6 @@ class Request(_Request): accept_language = env.get('HTTP_ACCEPT_LANGUAGE') content = env.get('CONTENT', '') content_type = env.get('CONTENT_TYPE') - method = env.get('REQUEST_METHOD') - path = env.get('PATH_INFO') query = env.get('QUERY_STRING') user_agent = env.get('HTTP_USER_AGENT') @@ -53,20 +55,39 @@ class Request(_Request): except: cookie = http.cookies.SimpleCookie() - try: GET = urllib.parse.parse_qs(query) except: GET = {} errors.append('Unable to parse GET parameters from query string "{}"'.format(query)) + if method == 'POST': + try: + if content_type == 'application/x-www-form-urlencoded': + POST = urllib.parse.parse_qs(content) + else: + POST = {} + errors.append('Unable to parse POST parameters from content string "{}"'.format(content)) + + except: + POST = {} + errors.append('Unable to parse POST parameters from content string "{}"'.format(content)) + + else: + POST = {} + if method == 'GET': parameters = GET + elif method == 'POST': + parameters = POST + else: + parameters = None result = super().__new__( cls, env=env, GET=GET, + POST=POST, accept=accept, accept_encoding=accept_encoding, accept_language=accept_language, @@ -81,9 +102,18 @@ class Request(_Request): user_agent=user_agent, ) - result.subpath = path + if path.startswith('/'): + result.subpath = path[1:] + else: + result.subpath = path + return result +def _get_request_from_env(env): + method = env.get('REQUEST_METHOD') + path = env.get('PATH_INFO') + return Request(method, path, env) + _Response = collections.namedtuple( 'Response', ( @@ -102,8 +132,8 @@ class Response(_Response): content_type = kwargs.pop('content_type') assert isinstance(content_type, str) - extra_headers = kwargs.pop('extra_headers', ()) - assert isinstance(extra_headers, tuple) + extra_headers = kwargs.pop('extra_headers', {}) + assert isinstance(extra_headers, dict) assert len(kwargs) == 0 @@ -117,9 +147,25 @@ class Response(_Response): @property def headers(self): - return ( - ('Content-Type', self.content_type), - ) + # Start with the defaults + result = { + 'X-Content-Type-Options': 'nosniff', + } + + result = {**result, **(self.extra_headers)} + + builtin_headers = { + 'Content-Type': self.content_type, + } + + for key, value in builtin_headers: + if key in result: + raise Exception('Header "{}" defined twice'.format(key)) + else: + result[key] = value + + return tuple(sorted(result.items())) + class HTMLResponse(Response): def __new__(cls, content, **kwargs): @@ -191,6 +237,38 @@ class RedirectResponse(_RedirectResponse): def content(self): return (b'',) +def default_file_not_found_handler(request): + return TextResponse( + 'Path "{}" with query "{}" not found'.format(request.path, request.query), + status=404, + ) + +def route_on_subpath(**kwargs): + routes = kwargs.pop('routes') + file_not_found_handler = kwargs.pop( + 'file_not_found_handler', + default_file_not_found_handler, + ) + + if routes is None: + raise Exception('Keyword argument "routes" is required') + + if len(kwargs) > 0: + raise Exception('Unexpected keyword argument') + + def wrapped(request): + split_subpath = request.subpath.split('/', 1) + subpath = split_subpath[0] + + if len(split_subpath) == 2: + request.subpath = split_subpath[1] + else: + request.subpath = '' + + return routes.get(subpath, file_not_found_handler)(request) + + return wrapped + REQUEST_METHODS = ( 'GET', 'HEAD', @@ -204,7 +282,7 @@ REQUEST_METHODS = ( ) def default_method_not_allowed_handler(request): - return Response('') + return TextResponse('', status=405) def default_options_handler(handlers): def handler(request): @@ -237,9 +315,42 @@ def route_on_method(**kwargs): def _get_status(response): return { + 100: '100 Continue', + 101: '101 Switching Protocols', 200: '200 OK', + 201: '201 Created', + 202: '202 Accepted', + 203: '203 Non-Authoritative Information', + 204: '204 No Content', + 205: '205 Reset Content', + 300: '300 Multiple Choices', + 301: '301 Moved Permanently', + 304: '304 Not Modified', 307: '307 Temporary Redirect', 308: '308 Permanent Redirect', + 400: '400 Bad Request', + 401: '401 Unauthorized', + 402: '402 Payment Required', + 403: '403 Forbidden', + 404: '404 Not Found', + 405: '405 Method Not Allowed', + 406: '406 Not Acceptable', + 409: '409 Conflict', + 410: '410 Gone', + 411: '411 Length Required', + 412: '412 Precondition Failed', + 413: '413 Payload Too Large', + 414: '414 URI Too Long', + 415: '415 Unsupported Media Type', + 416: '416 Range Not Satisfiable', + 417: '417 Expectation Failed', + 418: "418 I'm a teapot", + 429: '429 Too Many Requests', + 431: '431 Request Header Fields Too Large', + 451: '451 Unavailable For Legal Reasons', + 500: '500 Internal Server Error', + 501: '501 Not Implemented', + 503: '503 Service Unavailable', }[response.status] def _get_headers(response): @@ -258,7 +369,7 @@ def _get_content(response): def App(handler): def app(env, start_fn): - response = handler(Request(env)) + response = handler(_get_request_from_env(env)) start_fn(_get_status(response), _get_headers(response)) return _get_content(response)