kit-sub: ответы по HTTP/1.1, как у nginx

В режиме --multi-port kit-sub отвечал строкой статуса HTTP/1.0 и «Server: nginx »
с пробелом. Теперь protocol_version = HTTP/1.1, заголовок Server без хвоста;
на ответы, после которых соединение закрывается (ошибки разбора, POST и т. п.),
добавлен Connection: close. Тест: tools/test/kitsub_http_test.py.
This commit is contained in:
maxdmitrov
2026-10-06 15:42:30 +03:00
parent 58fc3b4704
commit 42be16a5a2
3 changed files with 59 additions and 0 deletions
+1
View File
@@ -11,6 +11,7 @@
* Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранных сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт, проверено клиентами Mihomo и Xray. Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя.
* Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу. А файлы настроек из копии `--restore` теперь читает строго по списку, как охранник на входе: лишнее имя – и копия не принимается, ничего не меняется.
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
* `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела – как настоящий nginx, а не его двоюродный брат.
* JSON-подписка снова открывается в приложениях на Xray: там, где у vless не было `encryption`, подписка теперь дописывает `none`.
Сервер обновится сам этой ночью, нетерпеливым – `kit update`.
+8
View File
@@ -338,8 +338,12 @@ def apply_rules(clash_yaml, loaded):
class Handler(http.server.BaseHTTPRequestHandler):
server_version = "nginx"
sys_version = ""
protocol_version = "HTTP/1.1" # как у nginx; ответы всегда с Content-Length
timeout = 20 # зависшие соединения не держим
def version_string(self): # «Server: nginx» без хвостового пробела
return self.server_version
def setup(self):
# TLS-рукопожатие – в потоке запроса, а не в общем цикле приёма соединений.
# Без сертификата (за nginx, на 127.0.0.1) работаем по обычному HTTP.
@@ -378,11 +382,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.send_response(code)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(body)))
if self.close_connection:
self.send_header("Connection", "close")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def do_other(self):
# Тело запроса не читаем – значит, и соединение дальше не держим.
self.close_connection = True
self.send_html_error(404)
do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_other
+50
View File
@@ -0,0 +1,50 @@
"""Ответы kit-sub как у nginx (режим --multi-port): python3 tools/test/kitsub_http_test.py"""
import json, os, socket, tempfile, threading, unittest, importlib.util
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
tmp = tempfile.mkdtemp()
cfgp = os.path.join(tmp, "config.json")
json.dump({"listen": "127.0.0.1", "port": 1, "path": "/sub/", "upstream": "http://127.0.0.1:1/"}, open(cfgp, "w"))
os.environ["KIT_SUB_CONFIG"] = cfgp
os.environ["KIT_SUB_RULES"] = os.path.join(tmp, "rules.yaml")
spec = importlib.util.spec_from_file_location("kit_sub", os.path.join(root, "scripts", "kit-sub.py"))
ks = importlib.util.module_from_spec(spec); spec.loader.exec_module(ks)
srv = ks.Server(("127.0.0.1", 0), ks.Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
def raw(data):
s = socket.create_connection(srv.server_address[:2]); s.settimeout(3); s.sendall(data); out = b""
try:
while True:
b = s.recv(65536)
if not b:
break
out += b
except socket.timeout:
out += b"<open>"
s.close()
return out.decode(errors="replace")
class Http(unittest.TestCase):
def test_status_and_server(self):
r = raw(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")
self.assertTrue(r.startswith("HTTP/1.1 404 Not Found\r\n"), r[:40])
self.assertIn("\r\nServer: nginx\r\n", r)
self.assertIn("\r\nContent-Length: ", r)
def test_keep_alive(self):
r = raw(b"GET / HTTP/1.1\r\nHost: x\r\n\r\nGET /a HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")
self.assertEqual(r.count("HTTP/1.1 404 Not Found"), 2)
self.assertNotIn("<open>", r)
def test_post_closes(self):
r = raw(b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhello")
self.assertTrue(r.startswith("HTTP/1.1 404"))
self.assertIn("Connection: close", r)
self.assertNotIn("<open>", r)
if __name__ == "__main__":
unittest.main()