18 Commits

Author SHA1 Message Date
beba1cad05 про ридми опять забыли( 2025-08-14 21:31:58 +00:00
36a76c4af7 added ability to change banlist file 2025-08-15 00:27:32 +03:00
4c8f908f18 added banlist 2025-08-15 00:25:49 +03:00
54c9ccef80 merge 1.2.2 2025-08-13 00:45:59 +03:00
0c1766d5d9 Merge branch 'testing'
penis
2025-08-13 00:44:38 +03:00
5527ef90e6 забыл про ридми 2025-08-13 00:40:52 +03:00
8c07d87efa какашки 2025-08-13 00:38:56 +03:00
96fd850b91 gfg 2025-08-11 12:03:01 +00:00
35353d63c8 cool patch with 5 lines of code 2025-08-11 11:55:14 +00:00
1d0ef1535f cool patch with 5 lines of code 2025-08-11 13:58:54 +03:00
a56a2bc4fb мерж 1.2.0 2025-08-10 18:06:39 +00:00
0984e169eb ebat obnova 2025-08-10 20:45:45 +03:00
055a3167e8 fixed gzaza 2025-08-09 20:32:27 +03:00
41d1984d5b мерж 1.1.3 2025-08-09 17:11:56 +00:00
4ed6833a9e da emae 2025-08-09 20:00:09 +03:00
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
8 changed files with 224 additions and 113 deletions

View File

@@ -1,48 +1,83 @@
## debweb # debweb
**debweb** - простой webserver для дебилов (for me) на асинхронных сокетах **debweb** - простой webserver для дебилов (for me) на асинхронных сокетах
## установка и настройка # установка и настройка
debweb использует всего одну стороннюю библиотеку - aiofiles. ее можно установить с помощью debweb использует всего одну стороннюю библиотеку - aiofiles. ее можно установить с помощью
```bash ```bash
pip install aiofiles pip install aiofiles
``` ```
> [!IMPORTANT]
конфигурация сия шедевра происходит в файле `config.py` конфигурация сия шедевра происходит в файле `config.py`
### основное ## основное
- `name` - название сервера, отображается в http заголовках - `name` - название сервера, отображается в http заголовках
- `proxied` - указывает, находится ли сервер за прокси
### сеть ## сеть
- `addr` - адрес сервера - `addr` - адрес сервера
- `port` - порт сервера - `port` - порт сервера
### файлы и директории ## файлы и директории
- `log_file` - файл логов (по умолчанию вывод в консоль) - `log_file` - файл логов (по умолчанию вывод в консоль)
- `preset_file` - файл пресета. обычный html документ. но в нем нужно указать одиночный тег `<FILES>` для отображения файлов в директории - `preset_file` - файл пресета
- `directory` - рабочая директория **обязательно с / на конце!!!!** - `banlist_file` - файл с айпишниками, которые будут получать 403
- `directory` - рабочая директория
### буферы ## буферы
- `read_buffer` - буфер для запроса - `read_buffer` - размер буфера для запросов
- `write_buffer` - размер буфера при отправке файлов - `write_buffer` - размер буфера при отправке файлов
### логи ## логи
- `start_msg` - лог при старте сервера - `start_msg` - лог при старте сервера
- `conn_msg` - лог при подключении - `conn_msg` - лог при подключении
- `get_msg` - лог при GET запросе - `get_msg` - лог при GET запросе
`<ADDR>` будет заменен на адрес клиента ## теги
`<FILE>` будет заменен на файл / директорию, к которой запрашивается доступ - `<ADDR>` - адрес клиента
- `<FILE>` - файл / директория, к которой запрашивается доступ
- `<TIME>` - время, когда был выполнен запрос
### ошибки ## шаблоны
- `e404_file` - html файл, который будет отправлен при ошибке 404 - `file_entry` - шаблон для генерации строк файлов в листинге директории
- `e404_msg` - лог при ошибке 404 - `dir_entry` - шаблон для генерации строк каталогов в листинге директории
- `time_format` - формат времени для всего документа
### теги шаблонов
- `<NAME>` - название элемента
- `<REL_PATH>` - относительный путь элемента
- `<CDATE>` - дата создания элемент
- `<MDATE>` - дата модификации элемента
- `<SIZE_B>` - размер файла в байтах
- `<SIZE_KB>` - размер файла в килобайтах
- `<SIZE_MB>` - размер файла в мегабайтах
- все остальные html теги
## preset.html
обычный html документ, являющийся шаблоном для листинга каталога. если в директории будет находиться preset.html, сервер будет использовать именно его. в противном случае - тот, который указан в конфиге.
### теги пресета
- `<FILES>` - отображает все элементы директории
- `<FILE_COUNT>` - количество файлов
- `<DIR_COUNT>` - количество подкаталогов
- `<TOTAL_COUNT>` - общее количество элементов
- `<SERVER>` - название сервера
- `<LOAD_TIME>` - время обработки страницы
- `<SERVER_TIME>` - время на сервере
## ошибки
- `err_Files` - словарь с кодами ошибок и файлами, которые отправляются при этих ошибках
- `err_msgs` - словарь с кодами ошибок и логами, которые отправляются при этих ошибках

0
banlist.lol Normal file
View File

View File

@@ -1,4 +1,4 @@
name="debweb 1.1.2" name="debweb 1.2.3"
proxied=False proxied=False
addr="localhost" addr="localhost"
@@ -6,6 +6,7 @@ port=7856
log_file=None log_file=None
preset_file="preset.html" preset_file="preset.html"
banlist_file="banlist.lol"
directory="files/" directory="files/"
read_buffer=16384 read_buffer=16384
@@ -15,5 +16,18 @@ start_msg="started at <ADDR>"
conn_msg="conn from <ADDR>" conn_msg="conn from <ADDR>"
get_msg="<ADDR> got <FILE>" get_msg="<ADDR> got <FILE>"
e404_file="404.html" file_entry = "<a href='/<REL_PATH>'><NAME></a> <SIZE_KB> <CDATE><br>\n"
e404_msg="<ADDR> err 404 <FILE>" dir_entry = "<a href='/<REL_PATH>'><NAME></a> <CDATE><br>\n"
time_format = "%a %b %e %H:%M:%S %Z %Y"
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>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</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!!! -->

211
main.py
View File

@@ -5,46 +5,67 @@ import datetime
import aiofiles import aiofiles
import asyncio import asyncio
import config import config
import utils
import time
import os import os
STATUS = {
200: "200 OK",
403: "403 Forbidden",
404: "404 Not Found",
405: "405 Method Not Allowed",
418: "418 I'm a teapot"
}
class WebServer: class WebServer:
def __init__(self):
self.name = config.name
self.proxied = config.proxied
self._addr = config.addr
self._port = config.port
self._log_file = config.log_file
self.preset_file = config.preset_file
self.directory = config.directory
self._read_buffer = config.read_buffer
self._write_size = config.write_buffer
self.conn_msg = config.conn_msg
self.start_msg = config.start_msg
self.get_msg = config.get_msg
self._e404_file = config.e404_file
self.e404_msg = config.e404_msg
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 "")
text = text.replace("<TIME>", datetime.datetime.now().strftime(config.time_format))
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)
rdata = await reader.read(self._read_buffer) conn_time = time.time()
rdata = await reader.read(config.read_buffer)
data = rdata.decode() data = rdata.decode()
if not data: return if not data: return
@@ -54,16 +75,37 @@ class WebServer:
real_addr = line[len("X-Real-IP: "):].strip() real_addr = line[len("X-Real-IP: "):].strip()
break break
if real_addr and self.proxied: if real_addr and config.proxied:
addr = (real_addr, addr[1]) addr = (real_addr, addr[1])
request = data.split("\n")[0] request = data.split("\n")[0]
parts = request.split() parts = request.split()
if len(parts) < 2: return if len(parts) < 2: return
path = unquote(parts[1]) path = unquote(parts[1])
file_name = path[1:] if path.startswith('/') else path file_name = path[1:] if path.startswith('/') else path
file_path = os.path.abspath(os.path.join(self.directory, file_name)) file_path = os.path.abspath(os.path.join(config.directory, file_name))
if not file_path.startswith(os.path.abspath(config.directory)): # directory traversal
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)
writer.close()
await writer.wait_closed()
return
if addr[0] in utils.get_banlist(config.banlist_file): # banlist for pidors
await self.log(config.err_msgs[403], addr, file_path)
file_size = os.path.getsize(config.err_files[403])
await self.send_headers(writer, 403, file_size)
await self.send_file(writer, config.err_files[403], file_size)
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)
@@ -71,93 +113,78 @@ 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"
f"Server: {self.name}\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: if os.path.isfile(os.path.join(file_path, "preset.html")):
preset_file = os.path.join(file_path, "preset.html")
else:
preset_file = config.preset_file
async with aiofiles.open(preset_file, "r", encoding="utf-8") as f:
resp = await f.read() resp = await f.read()
files = "" files = ""
base_path = os.path.relpath(file_path, self.directory).replace('\\', '/') base_path = os.path.relpath(file_path, config.directory).replace('\\', '/')
if base_path == '.': if base_path == '.': base_path = ''
base_path = ''
file_count = 0
dir_count = 0
for item in sorted(os.listdir(file_path)): for item in sorted(os.listdir(file_path)):
item_path = os.path.join(file_path, item) item_path = os.path.join(file_path, item)
is_dir = os.path.isdir(item_path) is_dir = os.path.isdir(item_path)
if base_path: if base_path: rel_path = f"{base_path}/{item}"
rel_path = f"{base_path}/{item}" else: rel_path = item
else:
rel_path = item
if is_dir: if is_dir:
rel_path += "/" rel_path += "/"
item += "/" item += "/"
dir_count += 1
modify_time = os.path.getmtime(item_path) else: file_count += 1
modify_datetime = datetime.datetime.fromtimestamp(modify_time)
formatted_time = modify_datetime.strftime("%d.%m.%Y %H:%M:%S")
rel_path_encoded = rel_path.replace(' ', '%20').replace('#', '%23') 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' entry = config.dir_entry if is_dir else config.file_entry
entry = entry.replace("<NAME>", item)
entry = entry.replace("<REL_PATH>", rel_path_encoded)
entry = entry.replace("<CDATE>", utils.get_create_time(item_path, config.time_format))
entry = entry.replace("<MDATE>", utils.get_mod_time(item_path, config.time_format))
entry = entry.replace("<SIZE_B>", f"{os.path.getsize(item_path)}B")
entry = entry.replace("<SIZE_KB>", f"{format(os.path.getsize(item_path) / 1024, ".2f")}KB")
entry = entry.replace("<SIZE_MB>", f"{format(os.path.getsize(item_path) / 1024 ** 2, ".2f")}MB")
files += entry
resp = resp.replace("<FILES>", files) resp = resp.replace("<FILES>", files)
resp = resp.replace("<FILE_COUNT>", str(file_count))
resp = resp.replace("<DIR_COUNT>", str(dir_count))
resp = resp.replace("<TOTAL_COUNT>", str(file_count + dir_count))
resp = resp.replace("<SERVER>", config.name)
resp = resp.replace("<LOAD_TIME>", format(time.time() - conn_time, ".3f"))
resp = resp.replace("<SERVER_TIME>", datetime.datetime.now().strftime(config.time_format))
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"Server: {self.name}\r\n"
f"Content-Length: {len(resp)}\r\n"
"\r\n"
)
writer.write(headers.encode() + resp) await self.send_headers(writer, 200, len(resp))
else:
await self.log(self.e404_msg, addr, file_path) writer.write(resp)
async with aiofiles.open(self._e404_file, "r", encoding="utf-8") as f:
content = await f.read()
response = (
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
f"Server: {self.name}\r\n"
"\r\n"
f"{content}"
)
writer.write(response.encode())
await writer.drain() await writer.drain()
else:
await self.log(config.err_msgs[404], addr, file_path)
file_size = os.path.getsize(config.err_files[404])
await self.send_headers(writer, 404, file_size)
await self.send_file(writer, config.err_files[404], file_size)
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
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() writer.close()
await writer.wait_closed() await writer.wait_closed()
@@ -165,10 +192,10 @@ class WebServer:
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()
@@ -176,13 +203,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()

17
utils.py Normal file
View File

@@ -0,0 +1,17 @@
import datetime
import os
def get_mod_time(path: str, format: str="%a %b %e %H:%M:%S %Z %Y") -> str:
modify_time = os.path.getmtime(path)
modify_datetime = datetime.datetime.fromtimestamp(modify_time)
return modify_datetime.strftime(format)
def get_create_time(path: str, format: str="%a %b %e %H:%M:%S %Z %Y") -> str:
create_time = os.path.getctime(path)
create_datetime = datetime.datetime.fromtimestamp(create_time)
return create_datetime.strftime(format)
def get_banlist(path: str) -> list:
return list(set(open("banlist.lol").readlines()))