diff --git a/Dockers/Dockerfile.sass.gemini b/Dockers/Dockerfile.sass.gemini new file mode 100644 index 0000000..f7cec21 --- /dev/null +++ b/Dockers/Dockerfile.sass.gemini @@ -0,0 +1,22 @@ +# Usamos la imagen oficial de Dart Sass basada en Alpine +FROM dart:stable AS build + +# Descargamos e instalamos la última versión de Dart Sass +RUN apt-get update && apt-get install -y curl tar +RUN curl -L https://github.com/sass/dart-sass/releases/download/1.101.0/dart-sass-1.101.0-linux-x64.tar.gz | tar -xzf - -C /opt + +# Imagen final limpia y ligera +FROM alpine:latest +RUN apk add --no-cache libc6-compat libstdc++ + +# Copiamos los binarios desde la etapa anterior +COPY --from=build /opt/dart-sass /opt/dart-sass + +# Añadimos SASS al PATH del contenedor +ENV PATH="/opt/dart-sass:${PATH}" + +# Configuramos el directorio de trabajo +WORKDIR /src + +# Permitimos que reciba los comandos directamente +ENTRYPOINT ["sass"] \ No newline at end of file diff --git a/JSON/AnP.settings.json b/JSON/AnP.settings.json index c3cb480..a05ca24 100644 --- a/JSON/AnP.settings.json +++ b/JSON/AnP.settings.json @@ -67,6 +67,136 @@ "default_routes_files" : "/JSON/AnP.routes.json", "AnP_RoutesManager_end" : null, + "AnP_HTTPAbstracts_start" : null, + "default_http_messages" : { + "100" : "Continue", + "101" : "Switching Protocols", + "102" : "Processing", + "103" : "Early Hints", + "200" : "OK", + "201" : "Created", + "202" : "Accepted", + "203" : "Non-Authoritative Information", + "204" : "No Content", + "205" : "Reset Content", + "206" : "Partial Content", + "207" : "Multi-Status", + "208" : "Already Reported", + "226" : "IM Used", + "300" : "Multiple Choices", + "301" : "Moved Permanently", + "302" : "Found", + "303" : "See Other", + "304" : "Not Modified", + "305" : "Use Proxy", + "306" : "Switch Proxy", + "307" : "Temporary Redirect", + "308" : "Permanent Redirect", + "400" : "Bad Request", + "401" : "Unauthorized", + "402" : "Payment Required", + "403" : "Forbidden", + "404" : "Not Found", + "405" : "Method Not Allowed", + "406" : "Not Acceptable", + "407" : "Proxy Authentication Required", + "408" : "Request Timeout", + "409" : "Conflict", + "410" : "Gone", + "411" : "Length Required", + "412" : "Precondition Failed", + "413" : "Payload Too Large", + "414" : "URI Too Long", + "415" : "Unsupported Media Type", + "416" : "Range Not Satisfiable", + "417" : "Expectation Failed", + "418" : "I'm a teapot", + "421" : "Misdirected Request", + "422" : "Unprocessable Content", + "423" : "Locked", + "424" : "Failed Dependency", + "425" : "Too Early", + "426" : "Upgrade Required", + "428" : "Precondition Required", + "429" : "Too Many Requests", + "431" : "Request Header Fields Too Large", + "451" : "Unavailable For Legal Reasons", + "500" : "Internal Server Error", + "501" : "Not Implemented", + "502" : "Bad Gateway", + "503" : "Service Unavailable", + "504" : "Gateway Timeout", + "505" : "HTTP Version Not Supported", + "506" : "Variant Also Negotiates", + "507" : "Insufficient Storage", + "508" : "Loop Detected", + "510" : "Not Extended", + "511" : "Network Authentication Required", + "218" : "This is fine", + "419" : "Page Expired", + "420" : "Method Failure", + "430" : "Request Header Fields Too Large", + "450" : "Blocked by Windows Parental Controls", + "498" : "Invalid Token", + "499" : "Token Required", + "509" : "Bandwidth Limit Exceeded", + "529" : "Site is overloaded", + "530" : "Site is frozen", + "540" : "Temporarily Disabled", + "598" : "Network read timeout error", + "599" : "Network Connect Timeout Error", + "783" : "Unexpected Token", + "440" : "Login Time-out", + "449" : "Retry With", + "444" : "No Response", + "494" : "Request header too large", + "495" : "SSL Certificate Error", + "496" : "SSL Certificate Required", + "497" : "HTTP Request Sent to HTTPS Port", + "520" : "Web Server Returned an Unknown Error", + "521" : "Web Server Is Down", + "522" : "Connection Timed Out", + "523" : "Origin Is Unreachable", + "524" : "A Timeout Occurred", + "525" : "SSL Handshake Failed", + "526" : "Invalid SSL Certificate", + "527" : "Railgun Error", + "561" : "Unauthorized", + "110" : "Response is Stale", + "111" : "Revalidation Failed", + "112" : "Disconnected Operation", + "113" : "Heuristic Expiration", + "199" : "Miscellaneous Warning", + "214" : "Transformation Applied", + "299" : "Miscellaneous Persistent Warning" + }, + "protocol" : "HTTP", + "protocol_version" : "1.1", + "default_protocol_code" : 200, + "http_server_name" : "AnP", + "http_server_version" : "0.0.1", + "default_cors" : "*", + "http_access_control_maximum_age" : 84600, + "http_keep_alive_timeout" : 5, + "http_keep_alive_maximum" : 100, + "default_http_charset" : "UTF-8", + "default_http_mime" : "text/html", + "http_accept_range" : "bytes", + "http_response_header" : [ + "{protocol}/{protocol_version} {protocol_code} {protocol_message}", + "Date: {date}", + "Server: {server}/{server_version}", + "Last-Modified: {last_modified} ", + "Accept-Ranges: {accept_range}", + "Content-Length: {response_length}", + "Access-Control-Max-Age: {access_control_max_age}", + "Keep-Alive: timeout={keep_alive_timeout}, max={keep_alive_maximum}", + "Access-Control-Allow-Origin: {cors}", + "Connection: Keep-Alive", + "Content-type: {mime}; charset={charset}" + ], + "AnP_HTTPAbstracts_end" : null, + "AnP_WebSocketsServersManager_start" : null, "default_web_sockets_servers" : { "anp" : { @@ -80,7 +210,8 @@ "AnP_HTTPServersManager_start" : null, "default_http_servers" : { "anp" : { - "type" : "HTTPDriver", + "type2" : "HTTPServerDriver", + "type" : "HTTPSocketServerDriver", "host" : "", "port" : 18000 } diff --git a/Python/Abstracts/HTTPServersAbstract.py b/Python/Abstracts/HTTPServersAbstract.py index e6d8c4e..6c0017f 100644 --- a/Python/Abstracts/HTTPServersAbstract.py +++ b/Python/Abstracts/HTTPServersAbstract.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from typing import Any, Self, Optional, Sequence from re import Match as REMath +import datetime from Interfaces.Application.AnPInterface import AnPInterface from Utils.Common import Common @@ -14,18 +15,37 @@ class HTTPServersAbstract(ABC): def __init__(self:Self, anp:AnPInterface, key:str, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None) -> None: self.anp:AnPInterface = anp - self.__inputs:dict[str, Any|None] = Common.get_dictionary(inputs) - self.host:str = self.DEFAULT_PORT - self.port:int = self.DEFAULT_HOST + self._inputs:dict[str, Any|None] = Common.get_dictionary(inputs) + self.host:str = self.DEFAULT_HOST + self.port:int = self.DEFAULT_PORT self._print_data:dict[str, Any|None] = { "port": self.port, "host": self.host } + self.http_messages:dict[int, str] = {int(code): message for code, message in anp.settings.get(( + "default_http_messages", "http_messages" + ), inputs).items()} + self.protocol:str = anp.settings.get(("http_protocol", "protocol"), inputs, "HTTP") + self.protocol_version:str = anp.settings.get(("http_protocol_version", "protocol_version"), inputs, "1.1") + self.accept_range:str = anp.settings.get(("http_accept_range", "accept_range"), inputs, "bytes") + self.access_control_max_age:int = anp.settings.get(("http_access_control_max_age", "access_control_max_age"), inputs, 84600) + self.keep_alive_maximum:int = anp.settings.get(("http_keep_alive_maximum", "keep_alive_maximum"), inputs, 100) + self.keep_alive_timeout:int = anp.settings.get(("http_keep_alive_timeout", "keep_alive_timeout"), inputs, 5) + self.cors:str = anp.settings.get(("http_cors", "cors"), inputs, "*") + self.mime:str = anp.settings.get(("default_http_mime", "http_mime", "mime"), inputs, "text/html") + self.charset:str = anp.settings.get(("default_http_charset", "http_charset", "charset"), inputs, "UTF-8") self._session_timeout:int = anp.settings.get(("sessions_timeout", "timeout"), inputs, 3600) self.key:str = key self.update() + @staticmethod + def format_datetime(datetime:datetime.datetime) -> str: + return datetime.strftime("%a, %d %b %Y %H:%M:%S GMT") + + def get_http_message(self:Self, code:int) -> str: + return self.http_messages.get(code, "Unknown Status Code") + def __update_print_data(self:Self) -> None: self._print_data["port"] = self.port self._print_data["host"] = self.host @@ -40,8 +60,8 @@ class HTTPServersAbstract(ABC): self.close() - self.port = self.anp.settings.get(("http_server_port", "http_port", "port"), self.__inputs, self.DEFAULT_PORT) - self.host = self.anp.settings.get(("http_server_host", "http_host", "host"), self.__inputs, self.DEFAULT_HOST) + self.port = self.anp.settings.get(("http_server_port", "http_port", "port"), self._inputs, self.DEFAULT_PORT) + self.host = self.anp.settings.get(("http_server_host", "http_host", "host"), self._inputs, self.DEFAULT_HOST) self.__update_print_data() self.anp.settings.get(("http_server_autostart", "http_autostart", "autostart")) and self.start() diff --git a/Python/Drivers/HTTPDriver.py b/Python/Drivers/HTTPServerDriver.py similarity index 98% rename from Python/Drivers/HTTPDriver.py rename to Python/Drivers/HTTPServerDriver.py index 35cabb9..b336d26 100644 --- a/Python/Drivers/HTTPDriver.py +++ b/Python/Drivers/HTTPServerDriver.py @@ -10,7 +10,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from http.cookies import SimpleCookie from Abstracts.HTTPServersAbstract import HTTPServersAbstract -class HTTPDriver(HTTPServersAbstract, ModelAbstract): +class HTTPServerDriver(HTTPServersAbstract, ModelAbstract): class HTTPRequestHandler(BaseHTTPRequestHandler): diff --git a/Python/Drivers/HTTPSocketServerDriver.py b/Python/Drivers/HTTPSocketServerDriver.py new file mode 100644 index 0000000..69fd009 --- /dev/null +++ b/Python/Drivers/HTTPSocketServerDriver.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Optional, Self, Sequence +from threading import Thread +from socket import ( + socket as Socket, + AF_INET as ADDRESS_FAMILY_IPV4, + SOCK_STREAM as SOCKET_STREAM, + SOL_SOCKET as SOCKET_LAYER, + SO_REUSEADDR as SOCKET_REUSE_ADDRESS +) +import datetime +from Interfaces.Application.AnPInterface import AnPInterface +from Abstracts.HTTPServersAbstract import HTTPServersAbstract +from Models.RequestModel import RequestModel +from Utils.Common import Common +from Utils.Checks import Check +from Utils.Patterns import RE + +class HTTPSocketServerDriver(HTTPServersAbstract): + + def __get(self:Self, keys:str|Sequence[str], default:Any|None = None) -> Any|None: + + real_keys:list[str] = [] + key:str + + for key in Common.get_keys(keys): + + header:str + + for header in ( + "http_socket_server_driver", + "http_server_driver", + "http_server", + "http", + "" + ): + + final_key:str = (header + "_" if header else "") + key + + if final_key not in keys: + real_keys.append(final_key) + + return self.anp.settings.get(real_keys, self._inputs, default) + + def __init__(self:Self, + anp:AnPInterface, + key:str, + inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None + ) -> None: + + self.__server:Socket|None = None + self.__thread:Thread|None = None + self.__working:bool = False + super().__init__(anp, key, inputs) + self.__maximum_connections:int = self.__get("maximum_connections", 5) + self.__cache_size:int = self.__get("cache_size", 1024) + self.__response_header:str = self.__get("response_header") + + def __listen(self:Self) -> None: + while self.__working: + try: + + client:Socket + address:str + port:int + data:bytes = b"" + header:str + body:str + header_lines:list[str] + line:str + request:RequestModel = RequestModel() + get_block:str + hash_block:str + response:bytes + response_body:bytes + + client, (address, port) = self.__server.accept() + + while True: + + buffer:bytes = client.recv(self.__cache_size) + + data += buffer + if len(buffer) < self.__cache_size: + break + + header, body = RE.DOUBLE_NEW_LINE.split(data.decode("utf-8", errors = "ignore"), 1) + header_lines = RE.NEW_LINE.split(header) + ( + request.method, + request.path, + get_block, + hash_block, + request.protocol, + request.protocol_version + ) = RE.HTTP_REQUEST.match(header_lines[0]).groups() + + for line in header_lines[1:]: + if RE.HTTP_HEADER_PARAMETER.match(line): + + key:str + value:str + + key, value = RE.HTTP_HEADER_PARAMETER.match(line).groups() + key = key.strip().lower() + value = value.strip() + if key in request.request_headers: + if Check.is_array(request.request_headers[key]): + request.request_headers[key].append(value) + else: + request.request_headers[key] = [request.request_headers[key], value] + else: + request.request_headers[key] = value + + request.get_variables = self.get_variables_from(get_block) + request.post_variables = self.get_variables_from(body) + request.cookies = self.load_cookies(request.request_headers.get("cookie")) + request.hash_variables = self.get_variables_from(hash_block) + + self.anp.routes.go([self.key], request.method, request.path, request) + + response_body = ( + request.response if Check.is_binary(request.response) else + request.response.encode() if Check.is_string(request.response) else + str(request.response).encode()) + response = Common.string_variables(self.__response_header, { + "content_length" : len(response_body), + "protocol" : request.get("protocol", request.protocol or self.protocol), + "protocol_version" : request.get("protocol_version", request.protocol_version or self.protocol_version), + "http_code" : request.response_code, + "http_message" : self.get_http_message(request.response_code), + "date" : self.format_datetime(datetime.datetime.now(datetime.timezone.utc)), + "last_modified" : self.format_datetime(request.last_modified or request.get("last_modified") or datetime.datetime.now(datetime.timezone.utc)), + "accept_range" : request.get("accept_range", self.accept_range), + "response_length" : len(response_body), + "access_control_max_age" : request.get("access_control_max_age", self.access_control_max_age), + "keep_alive_maximum" : request.get("keep_alive_maximum", self.keep_alive_maximum), + "keep_alive_timeout" : request.get("keep_alive_timeout", self.keep_alive_timeout), + "cors" : request.get("cors", self.cors), + "mime" : request.response_mime or request.get("mime", self.mime), + "charset" : request.response_charset or request.get("charset", self.charset) + }).encode() + response_body + + client.sendall(response) + client.close() + + except Exception as exception: + self.anp.exception(exception, "anp_http_socket_server_driver_run_exception", { + "key" : self.key, + "host" : self.host, + "port" : self.port + }) + + def start(self:Self) -> None: + + self.__working = True + self.__server = Socket(ADDRESS_FAMILY_IPV4, SOCKET_STREAM) + + try: + + self.__server.setsockopt(SOCKET_LAYER, SOCKET_REUSE_ADDRESS, 1) + self.__server.bind((self.host, self.port)) + self.__server.listen(self.__maximum_connections) + + self.__thread = Thread(target = self.__listen) + self.__thread.start() + + except Exception as exception: + self.anp.exception(exception, "anp_http_socket_server_driver_start_exception", { + "key" : self.key, + "host" : self.host, + "port" : self.port + }) + + def close(self:Self) -> None: + + self.__working = False + + if self.__server: + try: + self.__server.close() + except Exception as _: + pass + self.__server = None + + if self.__thread: + try: + self.__thread.join() + except Exception as _: + pass + self.__thread = None \ No newline at end of file diff --git a/Python/Models/RequestModel.py b/Python/Models/RequestModel.py index da67f0e..62682c2 100644 --- a/Python/Models/RequestModel.py +++ b/Python/Models/RequestModel.py @@ -13,6 +13,7 @@ class RequestModel: self.post_variables:dict[str, Any|None] = {} self.get_variables:dict[str, Any|None] = {} self.url_variables:dict[str, Any|None] = {} + self.hash_variables:dict[str, Any|None] = {} self.cookies:dict[str, Any|None] = {} self.variables:dict[str, Any|None] = {} self.request_headers:dict[str, Any|None] = {} @@ -20,11 +21,15 @@ class RequestModel: self.route:RouteAbstract|None = None self.response:str|bytes|None = None self.response_mime:str|None = None + self.response_charset:str|None = None self.response_code:int = 0 self.response_headers:dict[str, Any|None] = {} self.callback:Callable[[RequestModel, str|bytes|None], None]|None = None self.data:Any|None = None self.session:SessionModel|None = session + self.protocol:str|None = None + self.protocol_version:str|None = None + self.last_modified:str|None = None def get(self:Self, key:str|Sequence[str], default:Optional[Any] = None) -> Any|None: if self.session is not None: @@ -34,7 +39,13 @@ class RequestModel: if results is not None: return results return Common.get_value(key, ( - self.cookies, self.url_variables, self.get_variables, self.post_variables, self.variables + self.cookies, + self.url_variables, + self.get_variables, + self.post_variables, + self.variables, + self.request_headers, + self.hash_variables, ), default) def set_variables(self:Self, inputs:dict[str, Any|None], on:Optional[str] = None) -> None: @@ -43,6 +54,7 @@ class RequestModel: self.url_variables if on == "url" else self.get_variables if on == "get" else self.post_variables if on == "post" else + self.hash_variables if on == "hash" else self.variables).update(Common.get_dictionary(Common.load_json(inputs))) def set_response(self:Self, data:Any|None) -> None: diff --git a/Python/Utils/Patterns.py b/Python/Utils/Patterns.py index 03d9e36..d3afcce 100644 --- a/Python/Utils/Patterns.py +++ b/Python/Utils/Patterns.py @@ -15,4 +15,7 @@ class RE: EXCEPTION:REPattern = re_compile(r'^\s*File "([^"]+)", line ([0-9]+), in ([^\n]+)(.*|[\r\n]*)*$') NEW_LINE:REPattern = re_compile(r'\r\n|[\r\n]') HTTP_VARIABLE:REPattern = re_compile(r'([^=&]+)=([^&]*)') - PARENT_PATH:REPattern = re_compile(r'^(.*)[\/\\][^\/\\]+[\/\\]?$') \ No newline at end of file + PARENT_PATH:REPattern = re_compile(r'^(.*)[\/\\][^\/\\]+[\/\\]?$') + DOUBLE_NEW_LINE:REPattern = re_compile(r'(?:\r\n){2}|\r{2}|\n{2}') + HTTP_HEADER_PARAMETER:REPattern = re_compile(r'([^:]+):\s*(.+)') + HTTP_REQUEST:REPattern = re_compile(r'^([^\s]+)\s([^\s\?\#]+)(?:\?([^#]+))?(?:\#([^\s]+))?\s([^\/]+)\/([0-9\.]+)$') \ No newline at end of file diff --git a/Python/run.py b/Python/run.py index 1414507..477d4d5 100644 --- a/Python/run.py +++ b/Python/run.py @@ -5,7 +5,8 @@ from typing import Any from Application.AnP import AnP from Controllers.AIController import AIController from Drivers.WebSocketServerDriver import WebSocketServerDriver -from Drivers.HTTPDriver import HTTPDriver +from Drivers.HTTPSocketServerDriver import HTTPSocketServerDriver +from Drivers.HTTPServerDriver import HTTPServerDriver from Drivers.OllamaDriver import OllamaDriver from Drivers.FilesDriver import FilesDriver @@ -13,7 +14,8 @@ inputs:dict[str, dict[str, Any|None]] = { "default_models" : { "AIController" : AIController, "WebSocketServerDriver" : WebSocketServerDriver, - "HTTPDriver" : HTTPDriver, + "HTTPSocketServerDriver" : HTTPSocketServerDriver, + "HTTPServerDriver" : HTTPServerDriver, "OllamaDriver" : OllamaDriver, "FilesDriver" : FilesDriver } diff --git a/Tools/sass.sh b/Tools/sass.sh index bac3be8..9ff5605 100755 --- a/Tools/sass.sh +++ b/Tools/sass.sh @@ -1,3 +1,7 @@ #!/bin/bash directory=`dirname $(readlink -f "$0")` -sass $directory/../Public/scss/AnP.scss ../Public/scss/AnP.css; +# sass $directory/../Public/scss/AnP.scss $directory/../Public/scss/AnP.css +docker build -t dirt-sass -f $directory/../Dockers/Dockerfile.sass.gemini $directory +cd $directory/../Public/scss +docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) dirt-sass AnP.scss AnP.css; +cd $directory