Compare commits
15 Commits
7e3820fda0
...
1.2.0
Author | SHA1 | Date | |
---|---|---|---|
a56a2bc4fb | |||
0984e169eb | |||
055a3167e8 | |||
41d1984d5b | |||
4ed6833a9e | |||
d68ca4e787 | |||
4d8ad30541 | |||
a638f39cc5 | |||
ee4d1f90a3 | |||
d64a4db140 | |||
e2579d3179 | |||
0262aea690 | |||
45156ab4ae | |||
231a9282ea | |||
11fccc6fc7 |
65
README.md
65
README.md
@@ -1,48 +1,81 @@
|
||||
## debweb
|
||||
# debweb
|
||||
|
||||
**debweb** - простой webserver для дебилов (for me) на асинхронных сокетах
|
||||
|
||||
## установка и настройка
|
||||
# установка и настройка
|
||||
|
||||
debweb использует всего одну стороннюю библиотеку - aiofiles. ее можно установить с помощью
|
||||
|
||||
```bash
|
||||
pip install aiofiles
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
|
||||
конфигурация сия шедевра происходит в файле `config.py`
|
||||
|
||||
### основное
|
||||
## основное
|
||||
|
||||
- `name` - название сервера, отображается в http заголовках
|
||||
- `proxied` - указывает, находится ли сервер за прокси
|
||||
|
||||
### сеть
|
||||
## сеть
|
||||
|
||||
- `addr` - адрес сервера
|
||||
- `port` - порт сервера
|
||||
|
||||
### файлы и директории
|
||||
## файлы и директории
|
||||
|
||||
- `log_file` - файл логов (по умолчанию вывод в консоль)
|
||||
- `preset_file` - файл пресета. обычный html документ. но в нем нужно указать одиночный тег `<FILES>` для отображения файлов в директории
|
||||
- `directory` - рабочая директория **обязательно с / на конце!!!!**
|
||||
- `preset_file` - файл пресета
|
||||
- `directory` - рабочая директория
|
||||
|
||||
### буферы
|
||||
## буферы
|
||||
|
||||
- `read_buffer` - буфер для запроса
|
||||
- `read_buffer` - размер буфера для запросов
|
||||
- `write_buffer` - размер буфера при отправке файлов
|
||||
|
||||
### логи
|
||||
## логи
|
||||
|
||||
- `start_msg` - лог при старте сервера
|
||||
- `conn_msg` - лог при подключении
|
||||
- `get_msg` - лог при GET запросе
|
||||
|
||||
`<ADDR>` будет заменен на адрес клиента
|
||||
## теги
|
||||
|
||||
`<FILE>` будет заменен на файл / директорию, к которой запрашивается доступ
|
||||
- `<ADDR>` - адрес клиента
|
||||
- `<FILE>` - файл / директория, к которой запрашивается доступ
|
||||
|
||||
### ошибки
|
||||
## шаблоны
|
||||
|
||||
- `e404_file` - html файл, который будет отправлен при ошибке 404
|
||||
- `e404_msg` - лог при ошибке 404
|
||||
- `file_entry` - шаблон для генерации строк файлов в листинге директории
|
||||
- `dir_entry` - шаблон для генерации строк каталогов в листинге директории
|
||||
- `time_format` - формат времени для всего документа
|
||||
|
||||
### теги шаблонов
|
||||
|
||||
- `<NAME>` - название элемента
|
||||
- `<REL_PATH>` - относительный путь элемента
|
||||
- `<CDATE>` - дата создания элемент
|
||||
- `<MDATE>` - дата модификации элемента
|
||||
- `<SIZE_B>` - размер файла в байтах
|
||||
- `<SIZE_KB>` - размер файла в килобайтах
|
||||
- `<SIZE_MB>` - размер файла в мегабайтах
|
||||
- все остальные html теги
|
||||
|
||||
## preset.html
|
||||
|
||||
обычный html документ, являющийся шаблоном для листинга каталога
|
||||
|
||||
### теги пресета
|
||||
|
||||
- `<FILES>` - отображает все элементы директории
|
||||
- `<FILE_COUNT>` - количество файлов
|
||||
- `<DIR_COUNT>` - количество подкаталогов
|
||||
- `<TOTAL_COUNT>` - общее количество элементов
|
||||
- `<SERVER>` - название сервера
|
||||
- `<LOAD_TIME>` - время обработки страницы
|
||||
- `<SERVER_TIME>` - время на сервере
|
||||
|
||||
## ошибки
|
||||
|
||||
- `err_Files` - словарь с кодами ошибок и файлами, которые отправляются при этих ошибках
|
||||
- `err_msgs` - словарь с кодами ошибок и логами, которые отправляются при этих ошибках
|
19
config.py
19
config.py
@@ -1,4 +1,4 @@
|
||||
name="debweb 1.1.0"
|
||||
name="debweb 1.2.0"
|
||||
proxied=False
|
||||
|
||||
addr="localhost"
|
||||
@@ -15,5 +15,18 @@ start_msg="started at <ADDR>"
|
||||
conn_msg="conn from <ADDR>"
|
||||
get_msg="<ADDR> got <FILE>"
|
||||
|
||||
e404_file="404.html"
|
||||
e404_msg="<ADDR> err 404 <FILE>"
|
||||
file_entry = "<a href='/<REL_PATH>'><NAME></a> <SIZE_KB> <CDATE><br>\n"
|
||||
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
9
html/403.html
Normal 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/418.html
Normal file
9
html/418.html
Normal 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!!! -->
|
213
main.py
213
main.py
@@ -5,55 +5,96 @@ import datetime
|
||||
import aiofiles
|
||||
import asyncio
|
||||
import config
|
||||
import utils
|
||||
import time
|
||||
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:
|
||||
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:
|
||||
text = text.replace("<ADDR>", f"{addr[0]}:{addr[1]}" if addr else "")
|
||||
text = text.replace("<FILE>", file if file else "")
|
||||
|
||||
if self._log_file:
|
||||
async with aiofiles.open(self._log_file, mode="a") as file:
|
||||
if config.log_file:
|
||||
async with aiofiles.open(config.log_file, mode="a") as file:
|
||||
await file.write(text + "\n")
|
||||
else:
|
||||
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:
|
||||
addr = writer.get_extra_info("peername")
|
||||
await self.log(self.conn_msg, addr)
|
||||
data = await reader.read(self._read_buffer)
|
||||
data = unquote(data.decode())
|
||||
await self.log(config.conn_msg, addr)
|
||||
conn_time = time.time()
|
||||
|
||||
rdata = await reader.read(config.read_buffer)
|
||||
data = rdata.decode()
|
||||
if not data: return
|
||||
|
||||
real_addr = "".join([l.replace("X-Real-IP: ", "") for l in data.split("\n") if l.startswith("X-Real-IP: ")][0]).strip() # ТАКОЙ ГАВНАКОД XD
|
||||
if real_addr and self.proxied:
|
||||
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]
|
||||
file_name = request.split()[1][1:]
|
||||
file_path = os.path.join(self.directory, file_name)
|
||||
parts = request.split()
|
||||
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)
|
||||
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return
|
||||
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
mime, _ = mimetypes.guess_type(file_path)
|
||||
@@ -61,37 +102,21 @@ class WebServer:
|
||||
if not mime: mime = "application/octet-stream"
|
||||
if mime.startswith("text"): mime += "; charset=utf-8"
|
||||
|
||||
headers = (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
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)
|
||||
await self.send_headers(writer, 200, file_size, mime)
|
||||
await self.send_file(writer, file_path, file_size)
|
||||
|
||||
|
||||
elif os.path.isdir(file_path):
|
||||
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()
|
||||
|
||||
files = ""
|
||||
base_path = file_name.strip('/')
|
||||
base_path = os.path.relpath(file_path, config.directory).replace('\\', '/')
|
||||
if base_path == '.': base_path = ''
|
||||
|
||||
file_count = 0
|
||||
dir_count = 0
|
||||
for item in sorted(os.listdir(file_path)):
|
||||
item_path = os.path.join(file_path, item)
|
||||
is_dir = os.path.isdir(item_path)
|
||||
@@ -99,49 +124,63 @@ class WebServer:
|
||||
if base_path: rel_path = f"{base_path}/{item}"
|
||||
else: rel_path = item
|
||||
|
||||
if is_dir: rel_path += "/"
|
||||
|
||||
modify_time = os.path.getmtime(item_path)
|
||||
modify_datetime = datetime.datetime.fromtimestamp(modify_time)
|
||||
formatted_time = modify_datetime.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
files += f'<a href="/{rel_path}">{item}</a> | {formatted_time}<br>' # TODO: добавить кастом для инфы про файлики
|
||||
if is_dir:
|
||||
rel_path += "/"
|
||||
item += "/"
|
||||
dir_count += 1
|
||||
else: file_count += 1
|
||||
|
||||
rel_path_encoded = rel_path.replace(' ', '%20').replace('#', '%23')
|
||||
|
||||
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("<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()
|
||||
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 + resp)
|
||||
else:
|
||||
await self.log(self.e404_msg, addr, file_path)
|
||||
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"{open(self._e404_file, encoding="utf-8").read()}"
|
||||
)
|
||||
writer.write(response.encode())
|
||||
await self.send_headers(writer, 200, len(resp))
|
||||
|
||||
writer.write(resp)
|
||||
await writer.drain()
|
||||
|
||||
|
||||
else:
|
||||
await self.log(config.err_msgs[404], addr, config.err_files[404])
|
||||
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()
|
||||
await writer.wait_closed()
|
||||
|
||||
return
|
||||
|
||||
await self.log(self.get_msg, addr, file_path)
|
||||
await writer.drain()
|
||||
await self.log(config.get_msg, addr, file_path)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
|
||||
async def start(self) -> None:
|
||||
server = await asyncio.start_server(
|
||||
self.handle,
|
||||
self._addr,
|
||||
self._port
|
||||
config.addr,
|
||||
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:
|
||||
await server.serve_forever()
|
||||
@@ -149,13 +188,13 @@ class WebServer:
|
||||
|
||||
async def main():
|
||||
s = WebServer()
|
||||
if not s.directory:
|
||||
if not config.directory:
|
||||
print("directory not set!")
|
||||
exit(1)
|
||||
if not s.preset_file:
|
||||
if not config.preset_file:
|
||||
print("preset file not set!")
|
||||
exit(1)
|
||||
if not os.path.isfile(s.preset_file):
|
||||
if not os.path.isfile(config.preset_file):
|
||||
print("invalid preset file")
|
||||
exit(1)
|
||||
await s.start()
|
||||
|
13
utils.py
Normal file
13
utils.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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)
|
Reference in New Issue
Block a user