From adb8defdfed7b5ae5c2994370e336de04b5cee2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 29 Jun 2026 14:48:07 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20DrHider=20=E2=80=94=20CSV=20in=20ZIP,=20?= =?UTF-8?q?double-encode=20fix,=20multipart=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/convert_server.py | 58 ++++++++++++++++++++++++++++++-------- deploy/services/drhider.py | 11 +++++--- site/services/drhider.py | 11 +++++--- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/deploy/convert_server.py b/deploy/convert_server.py index fb314b6..6bf6cb4 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -217,28 +217,64 @@ class Handler(BaseHTTPRequestHandler): def _handle_drhider(self): """Обфускация: multipart files → LLM-NER → ZIP.""" from services.drhider import obfuscate_files - from services.llm_client import HttpxLLMClient - from services.upload import parse_multipart length = int(self.headers.get("Content-Length", 0)) raw = self.rfile.read(length) ctype = self.headers.get("Content-Type", "") - files = parse_multipart(raw, ctype) + # Парсим multipart вручную + import cgi, re + _, params = cgi.parse_header(ctype) + boundary = params.get("boundary", "").encode() + if not boundary: + self._json({"ok": False, "error": "No multipart boundary"}, 400) + return + + files = [] + # Разбиваем по boundary + parts = raw.split(b"--" + boundary) + for part in parts: + if b"Content-Disposition" not in part: + continue + try: + header_end = part.index(b"\r\n\r\n") + except ValueError: + continue + headers_raw = part[:header_end].decode("latin-1", errors="replace") + content = part[header_end + 4:] + if content.endswith(b"\r\n"): + content = content[:-2] + # Извлечь filename + m = re.search(r'filename="([^"]*)"', headers_raw) + if not m: + continue + fname = m.group(1) + if not fname: + continue + files.append((fname, content, "")) + if not files: self._json({"ok": False, "error": "Нет файлов"}, 400) return - file_list = [(f.filename, f.content, f.content_type) for f in files] + # LLM-клиент (совместимый с drhider.LLMClient протоколом: complete(prompt)->str) + class _VMLLM: + def complete(self, prompt): + import httpx + r = httpx.post( + os.environ.get("LLM_URL", "https://api.aillm.ru/v1/chat/completions"), + json={"model": os.environ.get("LLM_MODEL", "gpt-oss-120b"), + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 8000, "temperature": 0.1}, + headers={"Authorization": f"Bearer {os.environ.get('LLM_KEY', '')}", + "Content-Type": "application/json"}, + timeout=120 + ) + r.raise_for_status() + return r.json()["choices"][0]["message"]["content"] try: - llm = HttpxLLMClient( - os.environ.get("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions"), - os.environ.get("LLM_API_KEY", ""), - os.environ.get("LLM_MODEL", "gpt-oss-120b") - ) - zip_data, _csv = obfuscate_files(file_list, llm_client=llm) - + zip_data, _csv = obfuscate_files(files, llm_client=_VMLLM()) self.send_response(200) self.send_header("Content-Type", "application/zip") self.send_header("Content-Disposition", 'attachment; filename="drhider_output.zip"') diff --git a/deploy/services/drhider.py b/deploy/services/drhider.py index 87fe3cf..846807c 100644 --- a/deploy/services/drhider.py +++ b/deploy/services/drhider.py @@ -218,12 +218,13 @@ class TwoPassObfuscator: else: # .doc или другой — замена по тексту txt = all_texts.get(fname, '') - obf_content = self._replace_in_text(txt, fname).encode('utf-8') + obf_content = self._replace_in_text(txt, fname) results.append((fname, obf_content)) # Собираем ZIP + CSV - return self._build_zip(results), self._build_mapping_csv() + csv_str = self._build_mapping_csv() + return self._build_zip(results, csv_str), csv_str finally: shutil.rmtree(self._workdir, ignore_errors=True) @@ -362,12 +363,14 @@ class TwoPassObfuscator: # --- Сборка выдачи --- - def _build_zip(self, files: List[Tuple[str, bytes]]) -> bytes: - """Собрать ZIP с обфусцированными файлами.""" + def _build_zip(self, files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes: + """Собрать ZIP с обфусцированными файлами + mapping.csv.""" buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: for fname, content in files: zf.writestr(fname, content) + if mapping_csv: + zf.writestr("mapping.csv", mapping_csv.encode('utf-8')) return buf.getvalue() def _build_mapping_csv(self) -> str: diff --git a/site/services/drhider.py b/site/services/drhider.py index 87fe3cf..846807c 100644 --- a/site/services/drhider.py +++ b/site/services/drhider.py @@ -218,12 +218,13 @@ class TwoPassObfuscator: else: # .doc или другой — замена по тексту txt = all_texts.get(fname, '') - obf_content = self._replace_in_text(txt, fname).encode('utf-8') + obf_content = self._replace_in_text(txt, fname) results.append((fname, obf_content)) # Собираем ZIP + CSV - return self._build_zip(results), self._build_mapping_csv() + csv_str = self._build_mapping_csv() + return self._build_zip(results, csv_str), csv_str finally: shutil.rmtree(self._workdir, ignore_errors=True) @@ -362,12 +363,14 @@ class TwoPassObfuscator: # --- Сборка выдачи --- - def _build_zip(self, files: List[Tuple[str, bytes]]) -> bytes: - """Собрать ZIP с обфусцированными файлами.""" + def _build_zip(self, files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes: + """Собрать ZIP с обфусцированными файлами + mapping.csv.""" buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: for fname, content in files: zf.writestr(fname, content) + if mapping_csv: + zf.writestr("mapping.csv", mapping_csv.encode('utf-8')) return buf.getvalue() def _build_mapping_csv(self) -> str: