forked from s3lph/matemat
New request parsing (WIP: Documentation)
This commit is contained in:
parent
5bb1dfad21
commit
118de8bf95
16 changed files with 774 additions and 144 deletions
|
@ -6,4 +6,5 @@ API that can be used by 'pagelets' - single pages of a web service. If a reques
|
|||
server will attempt to serve the request with a static resource in a previously configured webroot directory.
|
||||
"""
|
||||
|
||||
from .requestargs import RequestArgument
|
||||
from .httpd import MatematWebserver, HttpHandler, pagelet
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
from typing import Any, Callable, Dict, List, Tuple, Union
|
||||
from typing import Any, Callable, Dict, Tuple, Union
|
||||
|
||||
import traceback
|
||||
|
||||
|
@ -13,6 +13,7 @@ from uuid import uuid4
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from matemat import __version__ as matemat_version
|
||||
from matemat.webserver import RequestArgument
|
||||
from matemat.webserver.util import parse_args
|
||||
|
||||
|
||||
|
@ -30,7 +31,7 @@ BaseHTTPRequestHandler.log_error = lambda self, fstring='', *args: None
|
|||
# Dictionary to hold registered pagelet paths and their handler functions
|
||||
_PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
|
||||
str, # Request path
|
||||
Dict[str, Tuple[str, Union[bytes, str, List[str]]]], # args: (name, (type, value))
|
||||
Dict[str, RequestArgument], # args: (name, argument)
|
||||
Dict[str, Any], # Session vars
|
||||
Dict[str, str]], # Response headers
|
||||
Tuple[int, Union[bytes, str]]]] = dict() # Returns: (status code, response body)
|
||||
|
@ -50,15 +51,14 @@ def pagelet(path: str):
|
|||
|
||||
(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])
|
||||
-> (int, Optional[Union[str, bytes]])
|
||||
|
||||
method: The HTTP method (GET, POST) that was used.
|
||||
path: The path that was requested.
|
||||
args: The arguments that were passed with the request (as GET or POST arguments), each of which may be
|
||||
either a str or bytes object, or a list of str.
|
||||
args: The arguments that were passed with the request (as GET or POST arguments).
|
||||
session_vars: The session storage. May be read from and written to.
|
||||
headers: The dictionary of HTTP response headers. Add headers you wish to send with the response.
|
||||
returns: A tuple consisting of the HTTP status code (as an int) and the response body (as str or bytes,
|
||||
|
@ -69,7 +69,7 @@ def pagelet(path: str):
|
|||
|
||||
def http_handler(fun: Callable[[str,
|
||||
str,
|
||||
Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
Dict[str, RequestArgument],
|
||||
Dict[str, Any],
|
||||
Dict[str, str]],
|
||||
Tuple[int, Union[bytes, str]]]):
|
||||
|
@ -181,7 +181,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
if session_id in self.server.session_vars:
|
||||
del self.server.session_vars[session_id]
|
||||
|
||||
def _handle(self, method: str, path: str, args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]]) -> None:
|
||||
def _handle(self, method: str, path: str, args: Dict[str, RequestArgument]) -> None:
|
||||
"""
|
||||
Handle a HTTP request by either dispatching it to the appropriate pagelet or by serving a static resource.
|
||||
|
||||
|
|
|
@ -8,4 +8,3 @@ from .main import main_page
|
|||
from .login import login_page
|
||||
from .logout import logout
|
||||
from .touchkey import touchkey_page
|
||||
from .upload_test import upload_test
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from matemat.exceptions import AuthenticationError
|
||||
from matemat.webserver import pagelet
|
||||
from matemat.webserver import pagelet, RequestArgument
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
@ -10,7 +10,7 @@ from matemat.db import MatematDatabase
|
|||
@pagelet('/login')
|
||||
def login_page(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
|
@ -43,15 +43,11 @@ def login_page(method: str,
|
|||
'''
|
||||
return 200, data.format(msg=args['msg'] if 'msg' in args else '')
|
||||
elif method == 'POST':
|
||||
if 'username' not in args or not isinstance(args['username'], str):
|
||||
return 400, None
|
||||
if 'password' not in args or not isinstance(args['password'], str):
|
||||
return 400, None
|
||||
username: str = args['username']
|
||||
password: str = args['password']
|
||||
username: RequestArgument = args['username']
|
||||
password: RequestArgument = args['password']
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(username, password)
|
||||
user: User = db.login(username.get_str(), password.get_str())
|
||||
except AuthenticationError:
|
||||
headers['Location'] = '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.'
|
||||
return 301, bytes()
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from matemat.webserver import pagelet
|
||||
from matemat.webserver import pagelet, RequestArgument
|
||||
|
||||
|
||||
@pagelet('/logout')
|
||||
def logout(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from matemat.webserver import MatematWebserver, pagelet
|
||||
from matemat.webserver import MatematWebserver, pagelet, RequestArgument
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
@ -9,7 +9,7 @@ from matemat.db import MatematDatabase
|
|||
@pagelet('/')
|
||||
def main_page(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from matemat.exceptions import AuthenticationError
|
||||
from matemat.webserver import pagelet
|
||||
from matemat.webserver import pagelet, RequestArgument
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
@ -10,7 +10,7 @@ from matemat.db import MatematDatabase
|
|||
@pagelet('/touchkey')
|
||||
def touchkey_page(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
|
@ -42,15 +42,11 @@ def touchkey_page(method: str,
|
|||
'''
|
||||
return 200, data.format(username=args['username'] if 'username' in args else '')
|
||||
elif method == 'POST':
|
||||
if 'username' not in args or not isinstance(args['username'], str):
|
||||
return 400, None
|
||||
if 'touchkey' not in args or not isinstance(args['touchkey'], str):
|
||||
return 400, None
|
||||
username: str = args['username']
|
||||
touchkey: str = args['touchkey']
|
||||
username: RequestArgument = args['username']
|
||||
touchkey: RequestArgument = args['touchkey']
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(username, touchkey=touchkey)
|
||||
user: User = db.login(username.get_str(), touchkey=touchkey.get_str())
|
||||
except AuthenticationError:
|
||||
headers['Location'] = f'/touchkey?username={args["username"]}&msg=Please%20try%20again.'
|
||||
return 301, bytes()
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from matemat.webserver import pagelet
|
||||
|
||||
|
||||
@pagelet('/upload')
|
||||
def upload_test(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Union[str, bytes]],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
if method == 'GET':
|
||||
return 200, '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<form method="post" action="#" enctype="multipart/form-data">
|
||||
<input type="file" name="testfile" />
|
||||
<input type="hidden" name="teststatic" value="statictest" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
else:
|
||||
headers['Content-Type'] = 'text/plain'
|
||||
return 200, args.items().__str__()
|
121
matemat/webserver/requestargs.py
Normal file
121
matemat/webserver/requestargs.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
|
||||
class RequestArgument(object):
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None) -> None:
|
||||
self.__name: str = name
|
||||
self.__value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None
|
||||
if value is None:
|
||||
self.__value = []
|
||||
else:
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
self.__value = value[0]
|
||||
else:
|
||||
self.__value = value
|
||||
else:
|
||||
self.__value = value
|
||||
|
||||
@property
|
||||
def is_array(self) -> bool:
|
||||
return isinstance(self.__value, list)
|
||||
|
||||
@property
|
||||
def is_scalar(self) -> bool:
|
||||
return not isinstance(self.__value, list)
|
||||
|
||||
@property
|
||||
def is_view(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.__name
|
||||
|
||||
def get_str(self, index: int = None) -> Optional[str]:
|
||||
if self.is_array:
|
||||
if index is None:
|
||||
raise ValueError('index must not be None')
|
||||
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
||||
if isinstance(v[1], str):
|
||||
return v[1]
|
||||
elif isinstance(v[1], bytes):
|
||||
return v[1].decode('utf-8')
|
||||
else:
|
||||
if index is not None:
|
||||
raise ValueError('index must be None')
|
||||
if isinstance(self.__value[1], str):
|
||||
return self.__value[1]
|
||||
elif isinstance(self.__value[1], bytes):
|
||||
return self.__value[1].decode('utf-8')
|
||||
|
||||
def get_bytes(self, index: int = None) -> Optional[bytes]:
|
||||
if self.is_array:
|
||||
if index is None:
|
||||
raise ValueError('index must not be None')
|
||||
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
||||
if isinstance(v[1], bytes):
|
||||
return v[1]
|
||||
elif isinstance(v[1], str):
|
||||
return v[1].encode('utf-8')
|
||||
else:
|
||||
if index is not None:
|
||||
raise ValueError('index must be None')
|
||||
if isinstance(self.__value[1], bytes):
|
||||
return self.__value[1]
|
||||
elif isinstance(self.__value[1], str):
|
||||
return self.__value[1].encode('utf-8')
|
||||
|
||||
def get_content_type(self, index: int = None) -> Optional[str]:
|
||||
if self.is_array:
|
||||
if index is None:
|
||||
raise ValueError('index must not be None')
|
||||
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
||||
return v[0]
|
||||
else:
|
||||
if index is not None:
|
||||
raise ValueError('index must be None')
|
||||
return self.__value[0]
|
||||
|
||||
def append(self, ctype: str, value: Union[str, bytes]):
|
||||
if self.is_view:
|
||||
raise TypeError('A RequestArgument view is immutable!')
|
||||
if len(self) == 0:
|
||||
self.__value = ctype, value
|
||||
else:
|
||||
if self.is_scalar:
|
||||
self.__value = [self.__value]
|
||||
self.__value.append((ctype, value))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__value) if self.is_array else 1
|
||||
|
||||
def __iter__(self):
|
||||
if self.is_scalar:
|
||||
yield _View(self.__name, self.__value)
|
||||
else:
|
||||
# Typing helper
|
||||
_value: List[Tuple[str, Union[bytes, str]]] = self.__value
|
||||
for v in _value:
|
||||
yield _View(self.__name, v)
|
||||
|
||||
def __getitem__(self, index: Union[int, slice]):
|
||||
if self.is_scalar:
|
||||
if index == 0:
|
||||
return _View(self.__name, self.__value)
|
||||
raise ValueError('Scalar RequestArgument only indexable with 0')
|
||||
return _View(self.__name, self.__value[index])
|
||||
|
||||
|
||||
class _View(RequestArgument):
|
||||
|
||||
def __init__(self, name: str, value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]]):
|
||||
super().__init__(name, value)
|
||||
|
||||
@property
|
||||
def is_view(self) -> bool:
|
||||
return True
|
|
@ -9,7 +9,7 @@ from abc import ABC
|
|||
from datetime import datetime
|
||||
from http.server import HTTPServer
|
||||
|
||||
from matemat.webserver.httpd import pagelet
|
||||
from matemat.webserver import pagelet, RequestArgument
|
||||
|
||||
|
||||
class HttpResponse:
|
||||
|
@ -158,14 +158,14 @@ def test_pagelet(path: str):
|
|||
|
||||
def with_testing_headers(fun: Callable[[str,
|
||||
str,
|
||||
Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
Dict[str, RequestArgument],
|
||||
Dict[str, Any],
|
||||
Dict[str, str]],
|
||||
Tuple[int, Union[bytes, str]]]):
|
||||
@pagelet(path)
|
||||
def testing_wrapper(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
status, body = fun(method, path, args, session_vars, headers)
|
||||
|
|
347
matemat/webserver/test/test_parse_request.py
Normal file
347
matemat/webserver/test/test_parse_request.py
Normal file
|
@ -0,0 +1,347 @@
|
|||
|
||||
import unittest
|
||||
|
||||
from matemat.webserver.util import parse_args
|
||||
|
||||
|
||||
class TestParseRequest(unittest.TestCase):
|
||||
|
||||
def test_parse_get_root(self):
|
||||
path, args = parse_args('/')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(0, len(args))
|
||||
|
||||
def test_parse_get_no_args(self):
|
||||
path, args = parse_args('/index.html')
|
||||
self.assertEqual('/index.html', path)
|
||||
self.assertEqual(0, len(args))
|
||||
|
||||
def test_parse_get_root_getargs(self):
|
||||
path, args = parse_args('/?foo=42&bar=1337&baz=Hello,%20World!')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(3, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual('text/plain', args['foo'].get_content_type())
|
||||
self.assertEqual('text/plain', args['bar'].get_content_type())
|
||||
self.assertEqual('text/plain', args['baz'].get_content_type())
|
||||
self.assertEqual('42', args['foo'].get_str())
|
||||
self.assertEqual('1337', args['bar'].get_str())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
||||
|
||||
def test_parse_get_getargs(self):
|
||||
path, args = parse_args('/abc/def?foo=42&bar=1337&baz=Hello,%20World!')
|
||||
self.assertEqual('/abc/def', path)
|
||||
self.assertEqual(3, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual('text/plain', args['foo'].get_content_type())
|
||||
self.assertEqual('text/plain', args['bar'].get_content_type())
|
||||
self.assertEqual('text/plain', args['baz'].get_content_type())
|
||||
self.assertEqual('42', args['foo'].get_str())
|
||||
self.assertEqual('1337', args['bar'].get_str())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
||||
|
||||
def test_parse_get_getarray(self):
|
||||
path, args = parse_args('/abc/def?foo=42&foo=1337&baz=Hello,%20World!')
|
||||
self.assertEqual('/abc/def', path)
|
||||
self.assertEqual(2, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_array)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual(2, len(args['foo']))
|
||||
self.assertEqual('42', args['foo'].get_str(0))
|
||||
self.assertEqual('1337', args['foo'].get_str(1))
|
||||
|
||||
def test_parse_get_zero_arg(self):
|
||||
path, args = parse_args('/abc/def?foo=&bar=42')
|
||||
self.assertEqual(2, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertEqual(1, len(args['foo']))
|
||||
self.assertEqual('', args['foo'].get_str())
|
||||
self.assertEqual('42', args['bar'].get_str())
|
||||
|
||||
def test_parse_get_urlencoded_encoding_fail(self):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_args('/?foo=42&bar=%80&baz=Hello,%20World!')
|
||||
|
||||
def test_parse_post_urlencoded(self):
|
||||
path, args = parse_args('/',
|
||||
postbody=b'foo=42&bar=1337&baz=Hello,%20World!',
|
||||
enctype='application/x-www-form-urlencoded')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(3, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual('text/plain', args['foo'].get_content_type())
|
||||
self.assertEqual('text/plain', args['bar'].get_content_type())
|
||||
self.assertEqual('text/plain', args['baz'].get_content_type())
|
||||
self.assertEqual('42', args['foo'].get_str())
|
||||
self.assertEqual('1337', args['bar'].get_str())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
||||
|
||||
def test_parse_post_urlencoded_array(self):
|
||||
path, args = parse_args('/',
|
||||
postbody=b'foo=42&foo=1337&baz=Hello,%20World!',
|
||||
enctype='application/x-www-form-urlencoded')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(2, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_array)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual(2, len(args['foo']))
|
||||
self.assertEqual('42', args['foo'].get_str(0))
|
||||
self.assertEqual('1337', args['foo'].get_str(1))
|
||||
|
||||
def test_parse_post_urlencoded_zero_arg(self):
|
||||
path, args = parse_args('/abc/def', postbody=b'foo=&bar=42', enctype='application/x-www-form-urlencoded')
|
||||
self.assertEqual(2, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertEqual(1, len(args['foo']))
|
||||
self.assertEqual('', args['foo'].get_str())
|
||||
self.assertEqual('42', args['bar'].get_str())
|
||||
|
||||
def test_parse_post_urlencoded_encoding_fail(self):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_args('/',
|
||||
postbody=b'foo=42&bar=%80&baz=Hello,%20World!',
|
||||
enctype='application/x-www-form-urlencoded')
|
||||
|
||||
def test_parse_post_multipart_no_args(self):
|
||||
path, args = parse_args('/',
|
||||
postbody=b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(0, len(args))
|
||||
|
||||
def test_parse_post_multipart(self):
|
||||
path, args = parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
self.assertEqual('/', path)
|
||||
self.assertEqual(3, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertIn('baz', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertTrue(args['baz'].is_scalar)
|
||||
self.assertEqual('text/plain', args['foo'].get_content_type())
|
||||
self.assertEqual('application/octet-stream', args['bar'].get_content_type())
|
||||
self.assertEqual('text/plain', args['baz'].get_content_type())
|
||||
self.assertEqual('42', args['foo'].get_str())
|
||||
self.assertEqual(b'1337', args['bar'].get_bytes())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
||||
|
||||
def test_parse_post_multipart_zero_arg(self):
|
||||
path, args = parse_args('/abc/def',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
self.assertEqual(2, len(args))
|
||||
self.assertIn('foo', args.keys())
|
||||
self.assertIn('bar', args.keys())
|
||||
self.assertTrue(args['foo'].is_scalar)
|
||||
self.assertTrue(args['bar'].is_scalar)
|
||||
self.assertEqual(1, len(args['foo']))
|
||||
self.assertEqual('', args['foo'].get_str())
|
||||
self.assertEqual('42', args['bar'].get_str())
|
||||
|
||||
def test_parse_post_multipart_broken_boundaries(self):
|
||||
with self.assertRaises(ValueError):
|
||||
# Boundary not defined in Content-Type
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data')
|
||||
with self.assertRaises(ValueError):
|
||||
# Corrupted "--" head at first boundary
|
||||
parse_args('/',
|
||||
postbody=b'-+testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
with self.assertRaises(ValueError):
|
||||
# Missing "--" tail at end boundary
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
with self.assertRaises(ValueError):
|
||||
# Missing Content-Type header in one part
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
with self.assertRaises(ValueError):
|
||||
# Missing Content-Disposition header in one part
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
with self.assertRaises(ValueError):
|
||||
# Missing form-data name argument
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
with self.assertRaises(ValueError):
|
||||
# Unknown Content-Disposition
|
||||
parse_args('/',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: attachment; name="bar"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
|
||||
def test_get_post_precedence_urlencoded(self):
|
||||
path, args = parse_args('/foo?foo=thisshouldnotbethere&bar=isurvived',
|
||||
postbody=b'foo=42&foo=1337&baz=Hello,%20World!',
|
||||
enctype='application/x-www-form-urlencoded')
|
||||
self.assertIn('foo', args)
|
||||
self.assertIn('bar', args)
|
||||
self.assertIn('baz', args)
|
||||
self.assertEqual(2, len(args['foo']))
|
||||
self.assertEqual(1, len(args['bar']))
|
||||
self.assertEqual(1, len(args['baz']))
|
||||
self.assertEqual('42', args['foo'].get_str(0))
|
||||
self.assertEqual('1337', args['foo'].get_str(1))
|
||||
self.assertEqual('isurvived', args['bar'].get_str())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
||||
|
||||
def test_get_post_precedence_multipart(self):
|
||||
path, args = parse_args('/foo?foo=thisshouldnotbethere&bar=isurvived',
|
||||
postbody=b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'42\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"; filename="bar.bin"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'1337\r\n'
|
||||
b'--testBoundary1337\r\n'
|
||||
b'Content-Disposition: form-data; name="baz"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'--testBoundary1337--\r\n',
|
||||
enctype='multipart/form-data; boundary=testBoundary1337')
|
||||
self.assertIn('foo', args)
|
||||
self.assertIn('bar', args)
|
||||
self.assertIn('baz', args)
|
||||
self.assertEqual(2, len(args['foo']))
|
||||
self.assertEqual(1, len(args['bar']))
|
||||
self.assertEqual(1, len(args['baz']))
|
||||
self.assertEqual('42', args['foo'].get_str(0))
|
||||
self.assertEqual('1337', args['foo'].get_str(1))
|
||||
self.assertEqual('isurvived', args['bar'].get_str())
|
||||
self.assertEqual('Hello, World!', args['baz'].get_str())
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
from matemat.webserver.httpd import HttpHandler
|
||||
from matemat.webserver import HttpHandler, RequestArgument
|
||||
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
|
||||
|
||||
import codecs
|
||||
|
@ -10,7 +10,7 @@ import codecs
|
|||
@test_pagelet('/just/testing/post')
|
||||
def post_test_pagelet(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
"""
|
||||
|
@ -18,13 +18,12 @@ def post_test_pagelet(method: str,
|
|||
"""
|
||||
headers['Content-Type'] = 'text/plain'
|
||||
dump: str = ''
|
||||
for k, (t, v) in args.items():
|
||||
if t.startswith('text/'):
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode('utf-8')
|
||||
dump += f'{k}: {",".join(v) if isinstance(v, list) else v}\n'
|
||||
for k, ra in args.items():
|
||||
for a in ra:
|
||||
if a.get_content_type().startswith('text/'):
|
||||
dump += f'{k}: {a.get_str()}\n'
|
||||
else:
|
||||
dump += f'{k}: {codecs.encode(v, "hex").decode("utf-8")}\n'
|
||||
dump += f'{k}: {codecs.encode(a.get_bytes(), "hex").decode("utf-8")}\n'
|
||||
return 200, dump
|
||||
|
||||
|
||||
|
@ -118,10 +117,14 @@ class TestPost(AbstractHttpdTest):
|
|||
kv: Dict[str, str] = dict()
|
||||
for l in lines:
|
||||
k, v = l.decode('utf-8').split(':', 1)
|
||||
kv[k.strip()] = v.strip() if ',' not in v else v.strip().split(',')
|
||||
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if k in kv:
|
||||
kv[k] += f',{v}'
|
||||
else:
|
||||
kv[k] = v
|
||||
# Make sure the arguments were properly parsed
|
||||
self.assertListEqual(['bar', 'baz'], kv['foo'])
|
||||
self.assertEqual('bar,baz', kv['foo'])
|
||||
self.assertEqual('1', kv['test'])
|
||||
|
||||
def test_post_urlenc_post_array(self):
|
||||
|
@ -141,10 +144,14 @@ class TestPost(AbstractHttpdTest):
|
|||
kv: Dict[str, str] = dict()
|
||||
for l in lines:
|
||||
k, v = l.decode('utf-8').split(':', 1)
|
||||
kv[k.strip()] = v.strip() if ',' not in v else v.strip().split(',')
|
||||
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if k in kv:
|
||||
kv[k] += f',{v}'
|
||||
else:
|
||||
kv[k] = v
|
||||
# Make sure the arguments were properly parsed
|
||||
self.assertListEqual(['bar', 'baz'], kv['foo'])
|
||||
self.assertEqual('bar,baz', kv['foo'])
|
||||
self.assertEqual('1', kv['test'])
|
||||
|
||||
def test_post_urlenc_mixed_array(self):
|
||||
|
@ -164,12 +171,16 @@ class TestPost(AbstractHttpdTest):
|
|||
kv: Dict[str, str] = dict()
|
||||
for l in lines:
|
||||
k, v = l.decode('utf-8').split(':', 1)
|
||||
kv[k.strip()] = v.strip() if ',' not in v else v.strip().split(',')
|
||||
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if k in kv:
|
||||
kv[k] += f',{v}'
|
||||
else:
|
||||
kv[k] = v
|
||||
# Make sure the arguments were properly parsed
|
||||
self.assertListEqual(['postbar', 'postbaz'], kv['foo'])
|
||||
self.assertListEqual(['1', '42'], kv['gettest'])
|
||||
self.assertListEqual(['1', '2'], kv['posttest'])
|
||||
self.assertEqual('postbar,postbaz', kv['foo'])
|
||||
self.assertEqual('1,42', kv['gettest'])
|
||||
self.assertEqual('1,2', kv['posttest'])
|
||||
|
||||
def test_post_no_body(self):
|
||||
"""
|
||||
|
@ -184,7 +195,7 @@ class TestPost(AbstractHttpdTest):
|
|||
|
||||
def test_post_multipart_post_only(self):
|
||||
"""
|
||||
Test a POST request with a miltipart/form-data body.
|
||||
Test a POST request with a miutipart/form-data body.
|
||||
"""
|
||||
# Send POST request
|
||||
formdata = (b'------testboundary\r\n'
|
||||
|
@ -211,34 +222,3 @@ class TestPost(AbstractHttpdTest):
|
|||
self.assertIn('bar', kv)
|
||||
self.assertEqual(kv['foo'], b'Hello, World!')
|
||||
self.assertEqual(kv['bar'], b'00010203040506070809800b0c730e0f')
|
||||
|
||||
def test_post_multipart_mixed(self):
|
||||
"""
|
||||
Test a POST request with a miltipart/form-data body.
|
||||
"""
|
||||
# Send POST request
|
||||
formdata = (b'------testboundary\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'Hello, World!\r\n'
|
||||
b'------testboundary\r\n'
|
||||
b'Content-Disposition: form-data; name="bar"; filename="foo.bar"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n\r\n'
|
||||
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x80\x0b\x0c\x73\x0e\x0f\r\n'
|
||||
b'------testboundary--\r\n')
|
||||
|
||||
self.client_sock.set_request(f'POST /just/testing/post?getfoo=bar&foo=thisshouldbegone HTTP/1.1\r\n'
|
||||
f'Content-Type: multipart/form-data; boundary=----testboundary\r\n'
|
||||
f'Content-Length: {len(formdata)}\r\n\r\n'.encode('utf-8') + formdata)
|
||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
||||
packet = self.client_sock.get_response()
|
||||
lines: List[bytes] = packet.body.split(b'\n')[:-1]
|
||||
kv: Dict[str, Any] = dict()
|
||||
for l in lines:
|
||||
k, v = l.split(b':', 1)
|
||||
kv[k.decode('utf-8').strip()] = v.strip()
|
||||
self.assertIn('foo', kv)
|
||||
self.assertIn('bar', kv)
|
||||
self.assertEqual(kv['getfoo'], b'bar')
|
||||
self.assertEqual(kv['foo'], b'Hello, World!')
|
||||
self.assertEqual(kv['bar'], b'00010203040506070809800b0c730e0f')
|
||||
|
|
204
matemat/webserver/test/test_requestargs.py
Normal file
204
matemat/webserver/test/test_requestargs.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
|
||||
from typing import List
|
||||
|
||||
import unittest
|
||||
|
||||
from matemat.webserver import RequestArgument
|
||||
# noinspection PyProtectedMember
|
||||
from matemat.webserver.requestargs import _View
|
||||
|
||||
|
||||
class TestRequestArguments(unittest.TestCase):
|
||||
|
||||
def test_create_default(self):
|
||||
ra = RequestArgument('foo')
|
||||
self.assertEqual('foo', ra.name)
|
||||
self.assertEqual(0, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
self.assertTrue(ra.is_array)
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_create_str_scalar(self):
|
||||
ra = RequestArgument('foo', ('text/plain', 'bar'))
|
||||
self.assertEqual('foo', ra.name)
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
self.assertFalse(ra.is_array)
|
||||
self.assertEqual('bar', ra.get_str())
|
||||
self.assertEqual(b'bar', ra.get_bytes())
|
||||
self.assertEqual('text/plain', ra.get_content_type())
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_str(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_bytes(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_content_type(0))
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_create_str_scalar_array(self):
|
||||
ra = RequestArgument('foo', [('text/plain', 'bar')])
|
||||
self.assertEqual('foo', ra.name)
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
self.assertFalse(ra.is_array)
|
||||
self.assertEqual('bar', ra.get_str())
|
||||
self.assertEqual(b'bar', ra.get_bytes())
|
||||
self.assertEqual('text/plain', ra.get_content_type())
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_str(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_bytes(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_content_type(0))
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_create_bytes_scalar(self):
|
||||
ra = RequestArgument('foo', ('application/octet-stream', b'\x00\x80\xff\xfe'))
|
||||
self.assertEqual('foo', ra.name)
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
self.assertFalse(ra.is_array)
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
ra.get_str()
|
||||
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
|
||||
self.assertEqual('application/octet-stream', ra.get_content_type())
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_str(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_bytes(0))
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual('bar', ra.get_content_type(0))
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_create_array(self):
|
||||
ra = RequestArgument('foo', [
|
||||
('text/plain', 'bar'),
|
||||
('application/octet-stream', b'\x00\x80\xff\xfe')
|
||||
])
|
||||
self.assertEqual('foo', ra.name)
|
||||
self.assertEqual(2, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
self.assertTrue(ra.is_array)
|
||||
with self.assertRaises(ValueError):
|
||||
ra.get_str()
|
||||
with self.assertRaises(ValueError):
|
||||
ra.get_bytes()
|
||||
with self.assertRaises(ValueError):
|
||||
ra.get_content_type()
|
||||
self.assertEqual('bar', ra.get_str(0))
|
||||
self.assertEqual(b'bar', ra.get_bytes(0))
|
||||
self.assertEqual('text/plain', ra.get_content_type(0))
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
ra.get_str(1)
|
||||
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
|
||||
self.assertEqual('application/octet-stream', ra.get_content_type(1))
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_append_empty_str(self):
|
||||
ra = RequestArgument('foo')
|
||||
self.assertEqual(0, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
|
||||
ra.append('text/plain', 'bar')
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
self.assertEqual('bar', ra.get_str())
|
||||
self.assertEqual(b'bar', ra.get_bytes())
|
||||
self.assertEqual('text/plain', ra.get_content_type())
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_append_empty_bytes(self):
|
||||
ra = RequestArgument('foo')
|
||||
self.assertEqual(0, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
|
||||
ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
ra.get_str()
|
||||
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
|
||||
self.assertEqual('application/octet-stream', ra.get_content_type())
|
||||
self.assertFalse(ra.is_view)
|
||||
|
||||
def test_append_multiple(self):
|
||||
ra = RequestArgument('foo')
|
||||
self.assertEqual(0, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
|
||||
ra.append('text/plain', 'bar')
|
||||
self.assertEqual(1, len(ra))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
|
||||
ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
|
||||
self.assertEqual(2, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
|
||||
ra.append('text/plain', 'Hello, World!')
|
||||
self.assertEqual(3, len(ra))
|
||||
self.assertFalse(ra.is_scalar)
|
||||
|
||||
def test_iterate_empty(self):
|
||||
ra = RequestArgument('foo')
|
||||
self.assertEqual(0, len(ra))
|
||||
for _ in ra:
|
||||
self.fail()
|
||||
|
||||
def test_iterate_scalar(self):
|
||||
ra = RequestArgument('foo', ('text/plain', 'bar'))
|
||||
self.assertTrue(ra.is_scalar)
|
||||
count: int = 0
|
||||
for it in ra:
|
||||
self.assertIsInstance(it, _View)
|
||||
self.assertEqual('foo', it.name)
|
||||
self.assertTrue(it.is_view)
|
||||
self.assertTrue(it.is_scalar)
|
||||
count += 1
|
||||
self.assertEqual(1, count)
|
||||
|
||||
def test_iterate_array(self):
|
||||
ra = RequestArgument('foo', [('text/plain', 'bar'), ('abc', b'def'), ('xyz', '1337')])
|
||||
self.assertFalse(ra.is_scalar)
|
||||
items: List[str] = list()
|
||||
for it in ra:
|
||||
self.assertIsInstance(it, _View)
|
||||
self.assertTrue(it.is_view)
|
||||
self.assertTrue(it.is_scalar)
|
||||
items.append(it.get_content_type())
|
||||
self.assertEqual(['text/plain', 'abc', 'xyz'], items)
|
||||
|
||||
def test_iterate_sliced(self):
|
||||
ra = RequestArgument('foo', [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j'), ('k', 'l')])
|
||||
self.assertFalse(ra.is_scalar)
|
||||
items: List[str] = list()
|
||||
for it in ra[1:4:2]:
|
||||
self.assertIsInstance(it, _View)
|
||||
self.assertTrue(it.is_view)
|
||||
self.assertTrue(it.is_scalar)
|
||||
items.append(it.get_content_type())
|
||||
self.assertEqual(['c', 'g'], items)
|
||||
|
||||
def test_index_scalar(self):
|
||||
ra = RequestArgument('foo', ('bar', 'baz'))
|
||||
it = ra[0]
|
||||
self.assertIsInstance(it, _View)
|
||||
self.assertEqual('foo', it.name)
|
||||
self.assertEqual('bar', it.get_content_type())
|
||||
self.assertEqual('baz', it.get_str())
|
||||
with self.assertRaises(ValueError):
|
||||
_ = ra[1]
|
||||
|
||||
def test_index_array(self):
|
||||
ra = RequestArgument('foo', [('a', 'b'), ('c', 'd')])
|
||||
it = ra[1]
|
||||
self.assertIsInstance(it, _View)
|
||||
self.assertEqual('foo', it.name)
|
||||
self.assertEqual('c', it.get_content_type())
|
||||
self.assertEqual('d', it.get_str())
|
||||
|
||||
def test_view_immutable(self):
|
||||
ra = RequestArgument('foo', ('bar', 'baz'))
|
||||
it = ra[0]
|
||||
self.assertIsInstance(it, _View)
|
||||
with self.assertRaises(TypeError):
|
||||
it.append('foo', 'bar')
|
|
@ -3,14 +3,14 @@ from typing import Any, Dict, Union
|
|||
|
||||
import os
|
||||
import os.path
|
||||
from matemat.webserver.httpd import HttpHandler
|
||||
from matemat.webserver import HttpHandler, RequestArgument
|
||||
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
|
||||
|
||||
|
||||
@test_pagelet('/just/testing/serve_pagelet_ok')
|
||||
def serve_test_pagelet_ok(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Union[bytes, str]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
headers['Content-Type'] = 'text/plain'
|
||||
|
@ -20,7 +20,7 @@ def serve_test_pagelet_ok(method: str,
|
|||
@test_pagelet('/just/testing/serve_pagelet_fail')
|
||||
def serve_test_pagelet_fail(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Union[bytes, str]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
session_vars['test'] = 'hello, world!'
|
||||
|
|
|
@ -4,14 +4,14 @@ from typing import Any, Dict, Union
|
|||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
|
||||
from matemat.webserver.httpd import HttpHandler
|
||||
from matemat.webserver import HttpHandler, RequestArgument
|
||||
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
|
||||
|
||||
|
||||
@test_pagelet('/just/testing/sessions')
|
||||
def session_test_pagelet(method: str,
|
||||
path: str,
|
||||
args: Dict[str, Union[bytes, str]],
|
||||
args: Dict[str, RequestArgument],
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]):
|
||||
session_vars['test'] = 'hello, world!'
|
||||
|
|
|
@ -3,8 +3,10 @@ from typing import Dict, List, Tuple, Optional, Union
|
|||
|
||||
import urllib.parse
|
||||
|
||||
from matemat.webserver import RequestArgument
|
||||
|
||||
def _parse_multipart(body: bytes, boundary: str) -> Dict[str, List[Tuple[str, Union[bytes, str]]]]:
|
||||
|
||||
def _parse_multipart(body: bytes, boundary: str) -> List[RequestArgument]:
|
||||
"""
|
||||
Given a HTTP body with form-data in multipart form, and the multipart-boundary, parse the multipart items and
|
||||
return them as a dictionary.
|
||||
|
@ -13,6 +15,8 @@ def _parse_multipart(body: bytes, boundary: str) -> Dict[str, List[Tuple[str, Un
|
|||
:param boundary: The multipart boundary.
|
||||
:return: A dictionary of field names as key, and content types and field values as value.
|
||||
"""
|
||||
# Prepend a CRLF for the first boundary to match
|
||||
body = b'\r\n' + body
|
||||
# Generate item header boundary and terminating boundary from general boundary string
|
||||
_boundary = f'\r\n--{boundary}\r\n'.encode('utf-8')
|
||||
_end_boundary = f'\r\n--{boundary}--\r\n'.encode('utf-8')
|
||||
|
@ -20,16 +24,15 @@ def _parse_multipart(body: bytes, boundary: str) -> Dict[str, List[Tuple[str, Un
|
|||
allparts = body.split(_end_boundary, 1)
|
||||
if len(allparts) != 2 or allparts[1] != b'':
|
||||
raise ValueError('Last boundary missing or corrupted')
|
||||
# Split remaining body into its parts (appending a CRLF for the first boundary to match), and verify at least 1 part
|
||||
# is there
|
||||
parts: List[bytes] = (b'\r\n' + allparts[0]).split(_boundary)
|
||||
# Split remaining body into its parts, and verify at least 1 part is there
|
||||
parts: List[bytes] = (allparts[0]).split(_boundary)
|
||||
if len(parts) < 1 or parts[0] != b'':
|
||||
raise ValueError('First boundary missing or corrupted')
|
||||
# Remove the first, empty part
|
||||
parts = parts[1:]
|
||||
|
||||
# Results go into this dict
|
||||
args: Dict[str, List[Tuple[str, Union[bytes, str]]]] = dict()
|
||||
args: Dict[str, RequestArgument] = dict()
|
||||
|
||||
# Parse each multipart part
|
||||
for part in parts:
|
||||
|
@ -50,25 +53,29 @@ def _parse_multipart(body: bytes, boundary: str) -> Dict[str, List[Tuple[str, Un
|
|||
cd, *cdargs = hdr['Content-Disposition'].split(';')
|
||||
# Content-Disposition MUST be form-data; everything else is rejected
|
||||
if cd.strip() != 'form-data':
|
||||
raise ValueError(f'Unknown Content-Disposition: cd')
|
||||
raise ValueError(f'Unknown Content-Disposition: {cd}')
|
||||
# Extract the "name" header argument
|
||||
has_name = False
|
||||
for cdarg in cdargs:
|
||||
k, v = cdarg.split('=', 1)
|
||||
if k.strip() == 'name':
|
||||
has_name = True
|
||||
name: str = v.strip()
|
||||
# Remove quotation marks around the name value
|
||||
if name.startswith('"') and name.endswith('"'):
|
||||
name = v[1:-1]
|
||||
# Add the Content-Type and the content to the header, with the provided name
|
||||
if name not in args:
|
||||
args[name] = list()
|
||||
args[name].append((hdr['Content-Type'].strip(), part))
|
||||
args[name] = RequestArgument(name)
|
||||
args[name].append(hdr['Content-Type'].strip(), part)
|
||||
if not has_name:
|
||||
raise ValueError('mutlipart/form-data part without name attribute')
|
||||
|
||||
return args
|
||||
return list(args.values())
|
||||
|
||||
|
||||
def parse_args(request: str, postbody: Optional[bytes] = None, enctype: str = 'text/plain') \
|
||||
-> Tuple[str, Dict[str, Tuple[str, Union[bytes, str, List[str]]]]]:
|
||||
-> Tuple[str, Dict[str, RequestArgument]]:
|
||||
"""
|
||||
Given a HTTP request path, and optionally a HTTP POST body in application/x-www-form-urlencoded or
|
||||
multipart/form-data form, parse the arguments and return them as a dictionary.
|
||||
|
@ -85,34 +92,41 @@ def parse_args(request: str, postbody: Optional[bytes] = None, enctype: str = 't
|
|||
# Parse the request "URL" (i.e. only the path)
|
||||
tokens = urllib.parse.urlparse(request)
|
||||
# Parse the GET arguments
|
||||
getargs = urllib.parse.parse_qs(tokens.query)
|
||||
if len(tokens.query) == 0:
|
||||
getargs = dict()
|
||||
else:
|
||||
getargs = urllib.parse.parse_qs(tokens.query, strict_parsing=True, keep_blank_values=True, errors='strict')
|
||||
|
||||
# TODO: { 'foo': [ ('text/plain', 'bar'), ('application/octet-stream', '\x80') ] }
|
||||
# TODO: Use a @dataclass once Python 3.7 is out
|
||||
args: Dict[str, Tuple[str, Union[bytes, str, List[str]]]] = dict()
|
||||
args: Dict[str, RequestArgument] = dict()
|
||||
for k, v in getargs.items():
|
||||
args[k] = 'text/plain', v
|
||||
args[k] = RequestArgument(k)
|
||||
for _v in v:
|
||||
args[k].append('text/plain', _v)
|
||||
|
||||
if postbody is not None:
|
||||
if enctype == 'application/x-www-form-urlencoded':
|
||||
# Parse the POST body
|
||||
postargs = urllib.parse.parse_qs(postbody.decode('utf-8'))
|
||||
pb: str = postbody.decode('utf-8')
|
||||
if len(pb) == 0:
|
||||
postargs = dict()
|
||||
else:
|
||||
postargs = urllib.parse.parse_qs(pb, strict_parsing=True, keep_blank_values=True, errors='strict')
|
||||
# Write all POST values into the dict, overriding potential duplicates from GET
|
||||
for k, v in postargs.items():
|
||||
args[k] = 'text/plain', v
|
||||
args[k] = RequestArgument(k)
|
||||
for _v in v:
|
||||
args[k].append('text/plain', _v)
|
||||
elif enctype.startswith('multipart/form-data'):
|
||||
# Parse the multipart boundary from the Content-Type header
|
||||
boundary: str = enctype.split('boundary=')[1]
|
||||
try:
|
||||
boundary: str = enctype.split('boundary=')[1].strip()
|
||||
except IndexError:
|
||||
raise ValueError('Multipart boundary in header not set or corrupted')
|
||||
# Parse the multipart body
|
||||
mpargs = _parse_multipart(postbody, boundary)
|
||||
for k, v in mpargs.items():
|
||||
# TODO: Process all values, not just the first
|
||||
args[k] = v[0]
|
||||
for ra in mpargs:
|
||||
args[ra.name] = ra
|
||||
else:
|
||||
raise ValueError(f'Unsupported Content-Type: {enctype}')
|
||||
# urllib.parse.parse_qs turns ALL arguments into arrays. This turns arrays of length 1 into scalar values
|
||||
for (k, (ct, v)) in args.items():
|
||||
if len(v) == 1:
|
||||
args[k] = ct, v[0]
|
||||
# Return the path and the parsed arguments
|
||||
return tokens.path, args
|
||||
|
|
Loading…
Reference in a new issue