13 Commits

Author SHA1 Message Date
d68ca4e787 suka 2025-08-09 19:35:12 +03:00
4d8ad30541 blya zabil versiu pomenyat 2025-08-09 19:31:42 +03:00
a638f39cc5 many things 2025-08-09 19:28:04 +03:00
ee4d1f90a3 мерж коммит 2025-08-07 20:59:30 +03:00
d64a4db140 yaay 2025-08-07 20:46:44 +03:00
e2579d3179 ya debil!!! 2025-08-07 16:55:20 +00:00
0262aea690 мерж пул реквест 1.1.1 2025-08-07 16:49:10 +00:00
45156ab4ae теперь точно все пофиксил 2025-08-07 19:41:38 +03:00
231a9282ea очередные фиксики этого говна 2025-08-07 19:26:49 +03:00
7e3820fda0 пофиксил прикол с полными путями 2025-08-07 19:10:47 +03:00
11fccc6fc7 мерж пул реквест 2025-08-07 15:11:22 +00:00
aafa84bfa3 еще больше говнокода! 2025-08-07 18:04:36 +03:00
780072bdbd обнова еее 2025-08-07 17:39:09 +03:00
8 changed files with 195 additions and 95 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
files/
__pycache__/

View File

@@ -10,35 +10,39 @@ debweb использует всего одну стороннюю библио
pip install aiofiles pip install aiofiles
``` ```
> [!IMPORTANT] > [!IMPORTANT]
конфигурация сия шедевра происходит с помощью env (переменных окружения). из обязательных - `PRESET_FILE` и `DIR` конфигурация сия шедевра происходит в файле `config.py`
### основное
- `name` - название сервера, отображается в http заголовках
### сеть ### сеть
- `ADDR` - адрес сервера (по умолчанию - `localhost`) - `addr` - адрес сервера
- `PORT` - порт сервера (по умолчанию - `7856`) - `port` - порт сервера
### файлы и директории ### файлы и директории
- `FILE` - файл логов (по умолчанию вывод в консоль) - `log_file` - файл логов (по умолчанию вывод в консоль)
- `PRESET_FILE` - файл пресета (подробнее ниже) - `preset_file` - файл пресета. обычный html документ. но в нем нужно указать одиночный тег `<FILES>` для отображения файлов в директории
- `DIR` - рабочая директория - `directory` - рабочая директория **обязательно с / на конце!!!!**
### буферы ### буферы
- `READ_BUFFER` - буфер для запроса (по умолчанию - `16384`) - `read_buffer` - буфер для запроса
- `WRITE_BUFFER` - размер чанка при отправке файлов (по умолчанию - `16384`) - `write_buffer` - размер буфера при отправке файлов
### логи ### логи
- `START_MSG` - лог при старте сервера (по умолчанию - `started at <ADDR>`) - `start_msg` - лог при старте сервера
- `CONN_MSG` - лог при подключении (по умолчанию - `conn from <ADDR>`) - `conn_msg` - лог при подключении
- `GET_MSG` - лог при GET запросе (по умолчанию - `<ADDR> got <FILE>`) - `get_msg` - лог при GET запросе
- `404_MSG` - лог при ошибке 404 (по умолчанию - `<ADDR> err 404 <FILE>`)
`<ADDR>` будет заменен на адрес клиента `<ADDR>` будет заменен на адрес клиента
`<FILE>` будет заменен на файл / директорию `<FILE>` будет заменен на файл / директорию, к которой запрашивается доступ
## preset ### ошибки
файл пресета - обычный html документ. но в нем нужно указать тег `<FILES>` для отображения файлов в директории. кастомизируй на здоровье! - `e404_file` - html файл, который будет отправлен при ошибке 404
- `e404_msg` - лог при ошибке 404

28
config.py Normal file
View File

@@ -0,0 +1,28 @@
name="debweb 1.1.3"
proxied=False
addr="localhost"
port=7856
log_file=None
preset_file="preset.html"
directory="files/"
read_buffer=16384
write_buffer=16384
start_msg="started at <ADDR>"
conn_msg="conn from <ADDR>"
get_msg="<ADDR> got <FILE>"
err_files = {
404: "html/404.html",
403: "html/403.html",
418: "html/418.html"
}
err_msgs = {
404: "<ADDR> err 404 <FILE>",
403: "<ADDR> err 403 <FILE>",
418: "<ADDR> err 418 (teapot) <FILE>"
}

9
html/403.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head><title>404 Forbidden</title></head>
<body>
<center><h1>404 Forbidden</h1></center>
<hr><center>debweb</center>
</body>
</html>
<!-- точно не спиздил с nginx'a!!! -->

9
html/404.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>debweb</center>
</body>
</html>
<!-- точно не спиздил с nginx'a!!! -->

9
html/418.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head><title>418 teapot</title></head>
<body>
<center><h1>пошолнахуй</h1></center>
<hr><center>debweb</center>
</body>
</html>
<!-- точно не спиздил с nginx'a!!! -->

192
main.py
View File

@@ -4,49 +4,96 @@ import mimetypes
import datetime import datetime
import aiofiles import aiofiles
import asyncio import asyncio
import time import config
import os import os
import re
STATUS = {
200: "200 OK",
403: "403 Forbidden",
404: "404 Not Found",
405: "405 Method Not Allowed",
418: "418 I'm a teapot"
}
# file_size = os.path.getsize(file_path)
class WebServer: class WebServer:
def __init__(self):
self._addr = os.environ.get("ADDR", "localhost")
self._port = int(os.environ.get("PORT", 7856))
self._log_file = os.environ.get("FILE")
self.preset_file = os.environ.get("PRESET_FILE")
self.directory = os.environ.get("DIR")
self._read_buffer = int(os.environ.get("READ_BUFFER", 16384))
self._write_size = int(os.environ.get("WRITE_BUFFER", 16384))
self.conn_msg = os.environ.get("CONN_MSG", "conn from <ADDR>")
self.start_msg = os.environ.get("START_MSG", "started at <ADDR>")
self.get_msg = os.environ.get("GET_MSG", "<ADDR> got <FILE>")
self.e404_msg = os.environ.get("404_MSG", "<ADDR> err 404 <FILE>")
async def log(self, text: str, addr: tuple=None, file: str=None) -> None: async def log(self, text: str, addr: tuple=None, file: str=None) -> None:
text = text.replace("<ADDR>", f"{addr[0]}:{addr[1]}" if addr else "") text = text.replace("<ADDR>", f"{addr[0]}:{addr[1]}" if addr else "")
text = text.replace("<FILE>", file if file else "") text = text.replace("<FILE>", file if file else "")
if self._log_file: if config.log_file:
async with aiofiles.open(self._log_file, mode="a") as file: async with aiofiles.open(config.log_file, mode="a") as file:
await file.write(text + "\n") await file.write(text + "\n")
else: else:
print(text) print(text)
async def send_headers(self, writer: asyncio.StreamWriter, status: int, file_size: int, mime: str="text/html; charset=utf-8") -> None:
headers = (
f"HTTP/1.1 {STATUS[status]}\r\n"
f"Content-Type: {mime}\r\n"
f"Content-Length: {file_size}\r\n"
f"Server: {config.name}\r\n"
"\r\n"
)
writer.write(headers.encode())
await writer.drain()
async def send_file(self, writer: asyncio.StreamWriter, file_path: str, file_size: int):
if not os.path.isfile(file_path):
writer.write("file error")
await writer.drain()
sent = 0
async with aiofiles.open(file_path, "rb") as f:
while sent < file_size:
chunk = await f.read(config.write_buffer)
if not chunk: break
writer.write(chunk)
await writer.drain()
sent += len(chunk)
async def handle(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: async def handle(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
addr = writer.get_extra_info("peername") addr = writer.get_extra_info("peername")
await self.log(self.conn_msg, addr) await self.log(config.conn_msg, addr)
data = await reader.read(self._read_buffer) rdata = await reader.read(config.read_buffer)
data = unquote(data.decode()) data = rdata.decode()
if not data: return if not data: return
real_addr = None
for line in data.split("\n"):
if line.startswith("X-Real-IP: "):
real_addr = line[len("X-Real-IP: "):].strip()
break
if real_addr and config.proxied:
addr = (real_addr, addr[1])
request = data.split("\n")[0] request = data.split("\n")[0]
file_name = request.split()[1][1:] parts = request.split()
file_path = self.directory + file_name if len(parts) < 2: return
path = unquote(parts[1])
file_name = path[1:] if path.startswith('/') else path
file_path = os.path.abspath(os.path.join(config.directory, file_name))
if not file_path.startswith(os.path.abspath(config.directory)):
await self.log(config.err_msgs[418], addr, file_path)
file_size = os.path.getsize(config.err_files[418])
await self.send_headers(writer, 418, file_size)
await self.send_file(writer, config.err_files[418], file_size)
await writer.close()
await writer.wait_closed()
return
if os.path.isfile(file_path): if os.path.isfile(file_path):
mime, _ = mimetypes.guess_type(file_path) mime, _ = mimetypes.guess_type(file_path)
@@ -54,80 +101,71 @@ class WebServer:
if not mime: mime = "application/octet-stream" if not mime: mime = "application/octet-stream"
if mime.startswith("text"): mime += "; charset=utf-8" if mime.startswith("text"): mime += "; charset=utf-8"
headers = ( await self.send_headers(writer, 200, file_size, mime)
"HTTP/1.1 200 OK\r\n", await self.send_file(writer, file_path, file_size)
f"Content-Type: {mime}\r\n",
f"Content-Length: {file_size}\r\n",
"\r\n"
)
writer.write("".join(headers).encode())
await writer.drain()
sent = 0
async with aiofiles.open(file_path, "rb") as f:
while sent < file_size:
chunk = await f.read(self._write_size)
if not chunk: break
writer.write(chunk)
await writer.drain()
sent += len(chunk)
elif os.path.isdir(file_path): elif os.path.isdir(file_path):
resp = "" resp = ""
async with aiofiles.open(self.preset_file, "r") as f: async with aiofiles.open(config.preset_file, "r", encoding="utf-8") as f:
resp = await f.read() resp = await f.read()
files = "" files = ""
for file_name in sorted(os.listdir(file_path)): # TODO: добавить настройки отображения файла base_path = os.path.relpath(file_path, config.directory).replace('\\', '/')
file = "/".join(file_path.split("/")[1:]) +"/" + file_name if base_path == '.':
base_path = ''
if os.path.isdir(file_path + "/" + file_name): for item in sorted(os.listdir(file_path)):
file_name = "/" + file_name item_path = os.path.join(file_path, item)
is_dir = os.path.isdir(item_path)
modify_time = (os.path.getmtime(file_path + "/" + file_name)) if base_path: rel_path = f"{base_path}/{item}"
else: rel_path = item
if is_dir:
rel_path += "/"
item += "/"
modify_time = os.path.getmtime(item_path)
modify_datetime = datetime.datetime.fromtimestamp(modify_time) modify_datetime = datetime.datetime.fromtimestamp(modify_time)
formatted_time = modify_datetime.strftime("%d.%m.%Y %H:%M:%S") formatted_time = modify_datetime.strftime("%d.%m.%Y %H:%M:%S")
files += f'<a href="/{file}">{file_name}</a> | {formatted_time}<br>' rel_path_encoded = rel_path.replace(' ', '%20').replace('#', '%23')
files += f'<a class={"dir" if is_dir else "file"} href="/{rel_path_encoded}">{item}</a> | {formatted_time}<br>\n'
resp = resp.replace("<FILES>", files) resp = resp.replace("<FILES>", files)
resp = resp.replace("//", "/")
resp = resp.encode() resp = resp.encode()
headers = (
"HTTP/1.1 200 OK\r\n"
f"Content-Type: text/html; charset=utf-8\r\n"
f"Content-Length: {len(resp)}\r\n"
"\r\n"
)
writer.write("".join(headers).encode() + resp) await self.send_headers(writer, 200, len(resp))
writer.write(resp)
await writer.drain()
else: else:
await self.log(self.e404_msg, addr, file_path) await self.log(self.e404_msg, addr, file_path)
response = ( # TODO: добавить страничку для 404 file_size = os.path.getsize(file_path)
"HTTP/1.1 404 Not Found\r\n" await self.send_headers(writer, 404, file_size)
"Content-Type: text/html; charset=utf-8\r\n" await self.send_file(writer, self._e404_file, file_size)
"\r\n"
"<h1>плоке плоке, 404</h1>" writer.close()
) await writer.wait_closed()
writer.write(response.encode())
await writer.drain()
return return
await self.log(self.get_msg, addr, file_path) await self.log(config.get_msg, addr, file_path)
await writer.drain() writer.close()
await writer.wait_closed()
async def start(self) -> None: async def start(self) -> None:
server = await asyncio.start_server( server = await asyncio.start_server(
self.handle, self.handle,
self._addr, config.addr,
self._port config.port
) )
await self.log(f"{self.start_msg}", (self._addr, self._port)) await self.log(f"{config.start_msg}", (config.addr, config.port))
async with server: async with server:
await server.serve_forever() await server.serve_forever()
@@ -135,13 +173,13 @@ class WebServer:
async def main(): async def main():
s = WebServer() s = WebServer()
if not s.directory: if not config.directory:
print("directory not set!") print("directory not set!")
exit(1) exit(1)
if not s.preset_file: if not config.preset_file:
print("preset file not set!") print("preset file not set!")
exit(1) exit(1)
if not os.path.isfile(s.preset_file): if not os.path.isfile(config.preset_file):
print("invalid preset file") print("invalid preset file")
exit(1) exit(1)
await s.start() await s.start()

1
preset.html Normal file
View File

@@ -0,0 +1 @@
<FILES>