diff --git a/Blazeio/Client/__init__.py b/Blazeio/Client/__init__.py index 514deb5..a7621d2 100644 --- a/Blazeio/Client/__init__.py +++ b/Blazeio/Client/__init__.py @@ -1,10 +1,9 @@ # Blazeio.Client -from asyncio import Protocol, get_event_loop, sleep +from asyncio import Protocol, get_event_loop, new_event_loop, sleep from time import perf_counter from ssl import create_default_context, SSLError from ujson import loads, dumps from collections import deque, defaultdict -from ..Dependencies import Log p = print @@ -16,10 +15,37 @@ def __init__(app, message=None): def __str__(app) -> str: return app.message +loop = get_event_loop() + +class Utl: + @classmethod + async def split_before(app, str_: (bytearray, bytes, str), to_find: (bytearray, bytes, str)): + await sleep(0) + if (idx := str_.find(to_find)) != -1: + return str_[:idx] + else: + return False + + @classmethod + async def split_between(app, str_: (bytearray, bytes, str), to_find: (bytearray, bytes, str)): + await sleep(0) + if (idx := str_.find(to_find)) != -1: + return (str_[:idx], str_[idx + len(to_find):]) + else: + return False + + @classmethod + async def split_after(app, str_: (bytearray, bytes, str), to_find: (bytearray, bytes, str)): + await sleep(0) + if (idx := str_.find(to_find)) != -1: + return str_[idx + len(to_find):] + else: + return False + class BlazeioClientProtocol(Protocol): def __init__(app, *args, **kwargs): app.buffer = deque() - app.response_headers = defaultdict(str) + app.response_headers = False app.__is_at_eof__ = False app.__is_connection_lost__ = False app.transport = None @@ -28,9 +54,10 @@ def __init__(app, *args, **kwargs): def connection_made(app, transport): app.transport = transport + app.transport.pause_reading() def data_received(app, data): - app.buffer.append(bytearray(data)) + app.buffer.append(data) def eof_received(app): app.__is_at_eof__ = True @@ -39,63 +66,64 @@ def connection_lost(app, exc=None): app.__is_connection_lost__ = True async def push(app, chunk): - if not isinstance(chunk, (bytes, bytearray)): - chunk = chunk.encode() - - if not app.__is_connection_lost__: - app.transport.write(chunk) - else: + if app.__is_connection_lost__: raise Err("Client has disconnected") + + app.transport.write(chunk) - async def pull(app, timeout=30): + async def pull(app, timeout=5): endl = b"\r\n0\r\n\r\n" - start_time = None - if app.response_headers: - app.expected_content_length = int(app.response_headers.get("Content-Length", "0")) - - app.received_content_length = 0 - else: - app.expected_content_length = False - app.received_content_length = 0 + if (cl := app.response_headers.get("Content-Length")): + app.expected_cl = int(cl) + else: + app.expected_cl = app.response_headers.get("Transfer-Encoding") + app.received_cl = 0 + else: + app.expected_cl = False + app.received_cl = 0 + + start = perf_counter() while True: await sleep(0) if app.buffer: - app.transport.pause_reading() - buff = app.buffer.popleft() - yield buff - - if endl in buff: break - if app.expected_content_length: - app.received_content_length += len(buff) - if app.received_content_length >= app.expected_content_length: - break + yield buff + + if app.response_headers: + app.received_cl += len(buff) + + elif endl in buff: break - start_time = perf_counter() + start = perf_counter() + else: - if start_time is not None: - if perf_counter() - float(start_time) >= timeout: - break - + if app.expected_cl and app.expected_cl != "chunked": + if app.received_cl >= app.expected_cl: break + else: + if perf_counter() - start >= timeout: break + if app.__is_connection_lost__: break - - if not app.transport.is_reading(): app.transport.resume_reading() - + yield None - app.transport.close() + if not app.transport.is_reading(): app.transport.resume_reading() + + #app.transport.close() async def fetch_headers(app, sepr = b"\r\n\r\n", head_sepr = b"\r\n"): tmp = bytearray() - app.response_headers + app.response_headers = defaultdict(str) + async for data in app.pull(): if data: tmp.extend(data) if (idx := tmp.rfind(sepr)) != -1: - app.buffer.appendleft(tmp[idx + len(sepr):]) + rem = tmp[idx + len(sepr):] + + app.buffer.appendleft(rem) tmp = tmp[:idx] break @@ -124,22 +152,52 @@ async def fetch_headers(app, sepr = b"\r\n\r\n", head_sepr = b"\r\n"): app.response_headers = dict(app.response_headers) return app.response_headers -loop = get_event_loop() - class Session: def __init__(app, **kwargs): app.protocols = deque() app.ssl_context = create_default_context() - async def fetch(app, + async def fetch(app, *args, **kwargs): return await app.prepare(*args, **kwargs) + + async def url_to_host( + app, + url: str, + sepr: str = "://", + sepr2: str = ":", + sepr3: str = "/" + ): + host = url + + if await Utl.split_before(host, "https"): + port: int = 443 + else: + port: int = 80 + + if (_ := await Utl.split_after(host, sepr)): + host = _ + else: + raise Err("%s is not a valid host" % host) + + if (_ := await Utl.split_between(host, sepr3)): + host = _[0] + path = sepr3 + _[1] + + if (__ := await Utl.split_between(host, sepr2)): + port = int(__[1]) + host = __[0] + else: + path = "/" + + return host, port, path + + async def prepare(app, url: str, method: str = "GET", headers = None, - connect_only=False, - params=None, - body=None + connect_only = False, ): host, port, path = await app.url_to_host(url) + if not headers: _headers_ = {} else: @@ -150,71 +208,18 @@ async def fetch(app, host=host, port=port, ssl=app.ssl_context if port == 443 else None ) - await protocol.push(f"{method} {path} HTTP/1.1\r\n") + await protocol.push(bytearray("%s %s HTTP/1.1\r\n" % (method, path), "utf-8")) if not "Host" in _headers_: _headers_["Host"] = host for key, val in _headers_.items(): - await protocol.push(f"{key}: {val}\r\n".encode()) + await protocol.push(bytearray("%s: %s\r\n" % (key, val), "utf-8")) - await protocol.push("\r\n".encode()) + await protocol.push(b"\r\n") - if connect_only: return protocol - - # if method in ["GET", "HEAD", "OPTIONS"]: - await protocol.fetch_headers() - + #if method in ("GET", "HEAD", "OPTIONS",): + if not connect_only: + await protocol.fetch_headers() return protocol - - async def url_to_host(app, url: str): - sepr = "://" - sepr2 = ":" - sepr3 = "/" - host = url - port = None - - if "https" in host: - port = 443 - else: - port = 80 - - if sepr in host: - host = host.split(sepr)[-1] - - if sepr2 in host: - _ = host.split(sepr2) - host, port_ = _[0], _[1] - - if sepr3 in port_: - port_ = port_.split(sepr3) - - if 1: - try: - port = int(port_[0]) - except Exception as e: - pass - - - - if sepr3 in host: - host = host.split(sepr3)[0] - - if sepr3 in url and len((_ := url.split(sepr3))) >= 3: - path = sepr3 + sepr3.join(_[3:]) - else: - path = sepr3 - - return host, port, path - - async def close(app): - return - try: - while app.protocols: - prot = app.protocols.popleft() - prot.transport.close() - - except SSLError as e: - return - except Exception as e: - p("Exception: " + str(e)) + \ No newline at end of file diff --git a/Blazeio/Client/__main__.py b/Blazeio/Client/__main__.py index 1ed3159..790b1e1 100644 --- a/Blazeio/Client/__main__.py +++ b/Blazeio/Client/__main__.py @@ -1,6 +1,8 @@ -from Client import Session +from Blazeio.Client import Session +from Blazeio.Dependencies import Log + from time import perf_counter -from asyncio import run, sleep, gather, create_task +from asyncio import run, sleep, gather, create_task, new_event_loop, get_event_loop class Test: async def test(app, url="http://example.com"): @@ -31,8 +33,24 @@ async def main(app): p(f"Duration: {perf_counter() - start:.4f} seconds") - + +async def test(): + client = Session() + i = await client.prepare("https://www.google.com/search") + + await Log.debug(i.response_headers) + + chunks = bytearray() + async for chunk in i.pull(): + if chunk: chunks.extend(chunk) + + await Log.debug(chunks) + + if __name__ == "__main__": #run(Test().test()) - run(Test().main()) + #run(Test().main()) + loop = get_event_loop() + + loop.run_until_complete(test()) \ No newline at end of file diff --git a/Blazeio/Client/__pycache__/__init__.cpython-311.pyc b/Blazeio/Client/__pycache__/__init__.cpython-311.pyc index 4ad2055..4fa58d5 100644 Binary files a/Blazeio/Client/__pycache__/__init__.cpython-311.pyc and b/Blazeio/Client/__pycache__/__init__.cpython-311.pyc differ diff --git a/Blazeio/Client/__pycache__/__init__.cpython-312.pyc b/Blazeio/Client/__pycache__/__init__.cpython-312.pyc index d7d0732..72c2c03 100644 Binary files a/Blazeio/Client/__pycache__/__init__.cpython-312.pyc and b/Blazeio/Client/__pycache__/__init__.cpython-312.pyc differ diff --git a/Blazeio/Client/__pycache__/__main__.cpython-312.pyc b/Blazeio/Client/__pycache__/__main__.cpython-312.pyc index 650634c..b28fdbe 100644 Binary files a/Blazeio/Client/__pycache__/__main__.cpython-312.pyc and b/Blazeio/Client/__pycache__/__main__.cpython-312.pyc differ diff --git a/Blazeio/Dependencies/__init__.py b/Blazeio/Dependencies/__init__.py index 6a84b40..063eb55 100644 --- a/Blazeio/Dependencies/__init__.py +++ b/Blazeio/Dependencies/__init__.py @@ -45,7 +45,6 @@ class Err(Exception): def __init__(app, message=None): - super().__init__(message) app.message = str(message) def __str__(app) -> str: @@ -53,7 +52,6 @@ def __str__(app) -> str: class ServerGotInTrouble(Exception): def __init__(app, message=None): - super().__init__(message) app.message = str(message) def __str__(app) -> str: diff --git a/Blazeio/Modules/request.py b/Blazeio/Modules/request.py index ae5f0a6..4ea8732 100644 --- a/Blazeio/Modules/request.py +++ b/Blazeio/Modules/request.py @@ -27,19 +27,25 @@ class Request: ] @classmethod - async def stream_chunks(app, r, MAX_BUFF_SIZE = None): - """ - Some systems have issues when you try writing bytearray to a file, so it is better to ensure youre streaming bytes object. - """ + async def stream_chunks(app, r): + if r.__buff__: + chunk = b'' + r.__buff__ + yield chunk - yield b'' + r.__buff__ + cl = int(r.headers.get("Content-Length", 0)) async for chunk in r.request(): yield chunk + if chunk is not None: + r.__received_length__ += len(chunk) + else: + if r.__received_length__ >= cl: break + @classmethod async def get_json(app, r, sepr = b'\r\n\r\n', sepr2 = b"{", sepr3 = b"}"): temp = bytearray() + async for chunk in app.stream_chunks(r): if chunk: temp.extend(chunk) @@ -99,19 +105,6 @@ async def get_params(app, r, q = "?", o = "&", y = "="): return dict(temp) - @classmethod - async def get_param(app, r, key: str): - key += "=" - - if not key in r.tail: - return - - param = r.tail.split(key)[-1] - - if o := "&" in param: param = param.split(o)[0] - - return await app.param_format(param) - @classmethod async def set_method(app, r, chunk, sepr1 = b' '): if (idx := chunk.find(sepr1)) != -1: @@ -155,18 +148,20 @@ async def get_headers(app, r, chunk, header_key_val = ': ', h_s = b'\r\n', mutat @classmethod async def set_data(app, r, sig = b"\r\n\r\n", max_buff_size = 10240): - r.__buff__ = bytearray() - async for chunk in r.request(): if chunk: r.__buff__.extend(chunk) if (idx := r.__buff__.find(sig)) != -1: data, r.__buff__ = r.__buff__[:idx], r.__buff__[idx + 4:] + r.__received_length__ += len(r.__buff__) + await app.set_method(r, data) break - elif len(r.__buff__) >= max_buff_size: break + elif len(r.__buff__) >= max_buff_size: + # Break out of the loop if exceeds limit without the sig being found + break return r @@ -200,7 +195,6 @@ async def get_form_data(app, r, start = b'form-data; name="', middle = b'"\r\n\r else: element = form_data - #for element in form_data.split(signal): if start in element and end in element: _ = element.split(start).pop().split(middle) diff --git a/Blazeio/Modules/server_tools.py b/Blazeio/Modules/server_tools.py index baf720e..25d511e 100644 --- a/Blazeio/Modules/server_tools.py +++ b/Blazeio/Modules/server_tools.py @@ -13,8 +13,7 @@ class Simpleserve: async def initialize(app, r, file: str, CHUNK_SIZE: int = 1024, **kwargs): if not exists(file): - await Deliver.text(r, "Not Found", 404, "Not Found") - return True + raise Abort("Not Found", 404, "Not Found") app.r, app.file, app.CHUNK_SIZE = r, file, CHUNK_SIZE @@ -24,12 +23,10 @@ async def initialize(app, r, file: str, CHUNK_SIZE: int = 1024, **kwargs): async def validate_cache(app): if app.r.headers.get("If-None-Match") == app.etag: - await Deliver.text(app.r, "Not Modified", 304, "Not Modified") - return True - + raise Abort("Not Modified", 304, "Not Modified") + elif (if_modified_since := app.r.headers.get("If-Modified-Since")) and strptime(if_modified_since, "%a, %d %b %Y %H:%M:%S GMT") >= gmtime(app.last_modified): - await Deliver.text(app.r, "Not Modified", 304, "Not Modified") - return True + raise Abort("Not Modified", 304, "Not Modified") async def prepare_metadata(app): app.file_size = getsize(app.file) diff --git a/Blazeio/Modules/streaming.py b/Blazeio/Modules/streaming.py index c997f29..d0dd5b8 100644 --- a/Blazeio/Modules/streaming.py +++ b/Blazeio/Modules/streaming.py @@ -67,24 +67,35 @@ async def HTTP_308(app, r, path, status=308, headers={}, reason="Redirect"): await r.prepare(headers, status=status, reason=reason) class Abort(Exception): - def __init__(app, message = None, status: int = 403, headers: dict = {}, **kwargs): - app.message = message - app.kwargs = kwargs - app.status = status - app.headers = headers - super().__init__(message) + def __init__( + app, + *args, + **kwargs + ): + app.args, app.kwargs = args, kwargs def __str__(app) -> str: - return str(message) - - async def respond(app, r): - if app.message is not None: - app.headers["Content-Type"] = "text/plain" + return str(app.message) + + async def text(app, r, message: str = "Something went wrong", status: int = 403, reason: str = "Forbidden", headers: dict = {} + ): + try: + headers_ = { + "Content-Type": "text/plain" + } + + if headers: headers_.update(headers) + + await r.prepare( + headers_, + status, + reason + ) + + await r.write(bytearray(message, "utf-8")) + + except Exception as e: + await Log.critical(r, e) - await r.prepare(app.headers, app.status, app.kwargs.get("reason", "Something went wrong")) - - if app.message is not None: - await r.write(app.message.encode()) - if __name__ == "__main__": pass diff --git a/Blazeio/__init__.py b/Blazeio/__init__.py index f96308e..5f8bd6a 100644 --- a/Blazeio/__init__.py +++ b/Blazeio/__init__.py @@ -54,17 +54,7 @@ async def prepare(cls, app, headers: dict = {}, status: int = 206, reason: str = @classmethod async def pull_multipart(cls, app, signal1 = b'------WebKitFormBoundary', signal2 = b'--\r\n'): - # This is important, some systems will crash if they try writing bytearray to disk, so convert bytearray to bytes with b'' + bytearray - - chunk = b'' + app.__buff__ - - if (idx := chunk.find(signal1)) != -1: - yield chunk[:idx] - return - - yield chunk - - async for chunk in app.request(): + async for chunk in Request.stream_chunks(app): if chunk: if (idx := chunk.find(signal2)) != -1: yield chunk[:idx] @@ -99,7 +89,8 @@ class BlazeioPayload(asyncProtocol): def __init__(app, on_client_connected): app.on_client_connected = on_client_connected app.__stream__ = deque() - app.__buff__ = None + app.__received_length__ = 0 + app.__buff__ = bytearray() app.__is_buffer_over_high_watermark__ = False app.__exploited__ = False app.__is_alive__ = True @@ -149,7 +140,7 @@ async def pull_multipart(app): async for chunk in BlazeioPayloadUtils.pull_multipart(app): yield chunk async def pull(app): - async for chunk in BlazeioPayloadUtils.pull_multipart(app): yield chunk + async for chunk in Request.stream_chunks(app): yield chunk class App: event_loop = loop @@ -290,7 +281,7 @@ async def serve_route(app, r): elif handle_all_middleware := app.declared_routes.get("handle_all_middleware"): await handle_all_middleware.get("func")(r) else: - await Deliver.text(r, "Not Found", 404, "Not Found") + raise Abort("Not Found", 404, "Not Found") if after_middleware := app.declared_routes.get("after_middleware"): await after_middleware.get("func")(r) @@ -300,12 +291,11 @@ async def handle_client(app, r): app.REQUEST_COUNT += 1 r.identifier = app.REQUEST_COUNT await app.serve_route(r) + except Abort as e: - try: - await e.respond(r) - except Exception as e: await Log.warning(r, e) + await e.text(r, *e.args, **e.kwargs) - except (Err, ServerGotInTrouble) as e: await Log.warning(e) + except (Err, ServerGotInTrouble) as e: await Log.warning(r, e.message) except (ConnectionResetError, BrokenPipeError, CancelledError, Exception) as e: await Log.critical(r, e) diff --git a/setup.py b/setup.py index 32ffc78..2ef8713 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ if environ.get("local"): version = "0.0.%s" % str(dt.now().timestamp()) else: - version = "2.0.0.0" + version = "2.0.0.1" setup( name="Blazeio",