From 21f0386072309b685fb143ee2d0da2d34d691aad Mon Sep 17 00:00:00 2001 From: RemiZOffAlex Date: Wed, 22 Apr 2026 11:47:41 +0300 Subject: [PATCH] Add content type parser --- .../domains/http/parsers/contenttype.py | 55 +++++++++++++++++++ src/nucleus/domains/http/parsers/request.py | 4 +- tests/test_request_parser.py | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/nucleus/domains/http/parsers/contenttype.py diff --git a/src/nucleus/domains/http/parsers/contenttype.py b/src/nucleus/domains/http/parsers/contenttype.py new file mode 100644 index 0000000..c534cec --- /dev/null +++ b/src/nucleus/domains/http/parsers/contenttype.py @@ -0,0 +1,55 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +class ContentType: + def __init__(self, **kwargs): + ''' + Parse a string into a ContentType object. + ''' + self.value = kwargs['value'] + self.media_type = kwargs['media_type'] + self.type = kwargs['type'] + self.subtype = kwargs['subtype'] + self.prefix = kwargs.get('prefix') + self.suffix = kwargs.get('suffix') + self.parameters = kwargs.get('parameters') + self.charset = kwargs.get('charset') + +def contenttype_parse(raw): + if not raw: + raise 'Content-Type is empty.' + parts = raw.split(';', 1) + media_type = parts[0].strip() + content_type, subtype = parts[0].split('/') + prefix = None + suffix = None + if '.' in subtype: + prefix, subtype = subtype.split('.') + + if '+' in subtype: + subtype, suffix = subtype.split('+') + + # Parse content type parameters + parameters = {} + charset = None + + for parameter in parts[1:]: + parameter = parameter.strip() + if '=' in parameter: + key, param_value = parameter.split('=', 1) + parameters[key.strip()] = param_value.strip() + if key.strip() == 'charset': + charset = param_value + else: + parameters[parameter] = True + + return ContentType( + value=raw, + media_type=media_type, + type=content_type, + prefix=prefix, + subtype=subtype, + suffix=suffix, + parameters=parameters, + charset=charset + ) diff --git a/src/nucleus/domains/http/parsers/request.py b/src/nucleus/domains/http/parsers/request.py index 77bc0d5..2aa581c 100644 --- a/src/nucleus/domains/http/parsers/request.py +++ b/src/nucleus/domains/http/parsers/request.py @@ -2,6 +2,7 @@ __author__ = 'RemiZOffAlex' __email__ = 'remizoffalex@mail.ru' from nucleus.domains.http.models.request import Request +from .contenttype import contenttype_parse from .json import parse_json @@ -67,7 +68,8 @@ def parse(raw): request.headers[key] = value if request.method=='POST': if 'Content-Type' in request.headers: - if request.headers['Content-Type']=='application/json; charset=UTF-8': + request.content_type = contenttype_parse(request.headers['Content-Type']) + if request.content_type.media_type=='application/json': request.json = parse_json(body) request.is_json = True return request diff --git a/tests/test_request_parser.py b/tests/test_request_parser.py index 2cf4138..d07262b 100644 --- a/tests/test_request_parser.py +++ b/tests/test_request_parser.py @@ -35,6 +35,7 @@ class TestRequestParse(unittest.TestCase): def test_parse_post_json(self): raw = b'POST /api HTTP/1.1\r\nHost: 10.2.0.1:9000\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36\r\nContent-Length: 277\r\nAccept: application/json, text/*\r\nAccept-Language: ru,en-US;q=0.9,en;q=0.8\r\nContent-Type: application/json; charset=UTF-8\r\nCookie: specialistoff_net=eyJpcCI6IjE5Mi4xNjguMzMuODgiLCJ1c2VyX2lkIjoxfQ.aMimlA.Lm8R-IvrbW_1MTtM5An50NH-154; session=eyJpcCI6IjE3Mi4xNi4xLjEiLCJ1c2VyX2lkIjoxfQ.acgSFg.5KkGM_fT5IDNodYcytMzJHd7prk\r\nX-Forwarded-For: 192.18.3.1\r\nX-Forwarded-Host: 10.2.0.1:9000\r\nX-Forwarded-Port: 9000\r\nX-Forwarded-Proto: http\r\nX-Forwarded-Server: 8dd1deb61302\r\nX-Real-Ip: 192.18.3.1\r\nAccept-Encoding: gzip\r\n\r\n[{"jsonrpc":"2.0","method":"questions","params":{"page":1,"order_by":{"field":"id","order":"desc"},"fields":["id","title","created","updated",{"tags":["id","name"],"user":["id","name"]}]},"id":"1099d48b231bb"},{"jsonrpc":"2.0","method":"questions.count","id":"41dbfec0441e68"}]' + raw = b'POST /api HTTP/1.1\r\nHost: 10.2.0.1:9000\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0\r\nContent-Length: 276\r\nAccept: application/json, text/*\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\r\nCache-Control: no-cache\r\nContent-Type: application/json; charset=utf-8\r\nOrigin: http://10.2.0.1:9000\r\nPragma: no-cache\r\nReferer: http://10.2.0.1:9000/questions\r\nX-Forwarded-For: 192.18.3.1\r\nX-Forwarded-Host: 10.2.0.1:9000\r\nX-Forwarded-Port: 9000\r\nX-Forwarded-Proto: http\r\nX-Forwarded-Server: 8dd1deb61302\r\nX-Real-Ip: 192.18.3.1\r\n\r\n[{"jsonrpc":"2.0","method":"questions","params":{"page":1,"order_by":{"field":"id","order":"desc"},"fields":["id","title","created","updated",{"tags":["id","name"],"user":["id","name"]}]},"id":"6759189805cce"},{"jsonrpc":"2.0","method":"questions.count","id":"821f0655b5d4d"}]' request = parse(raw) self.assertTrue(request.is_json)