mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
fix(docker): fix E2E torrent test and transmission-remote compatibility
- Add transmission-cli package (provides transmission-remote binary) - Fix SETUPTOOLS_SCM_PRETEND_VERSION placement before RUN make install - Fix PATH to use /root/.local/bin and include venv bin - Switch TransmissionSession.add() from watch-dir polling to transmission-remote -a - Remove --no-auth flag (unrecognized in transmission-remote 4.1.0) - Add test_docker_torrent_downloads_wordlists E2E test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c037242037
commit
90257cfb1b
+4
-1
@@ -13,6 +13,7 @@ RUN apt-get update \
|
|||||||
ocl-icd-libopencl1 \
|
ocl-icd-libopencl1 \
|
||||||
pocl-opencl-icd \
|
pocl-opencl-icd \
|
||||||
p7zip-full \
|
p7zip-full \
|
||||||
|
transmission-cli \
|
||||||
transmission-daemon \
|
transmission-daemon \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -20,9 +21,11 @@ RUN python -m pip install -q uv==0.9.28
|
|||||||
|
|
||||||
COPY . /workspace
|
COPY . /workspace
|
||||||
|
|
||||||
|
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
|
||||||
|
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
ENV PATH="${HOME}/.local/bin:${PATH}"
|
ENV PATH="/workspace/.venv/bin:/root/.local/bin:${PATH}"
|
||||||
ENV HATE_CRACK_SKIP_INIT=1
|
ENV HATE_CRACK_SKIP_INIT=1
|
||||||
|
|
||||||
CMD ["bash", "-lc", "${HOME}/.local/bin/hate_crack --help >/tmp/hc_help.txt && ./hate_crack.py --help >/tmp/hc_script_help.txt"]
|
CMD ["bash", "-lc", "${HOME}/.local/bin/hate_crack --help >/tmp/hc_help.txt && ./hate_crack.py --help >/tmp/hc_script_help.txt"]
|
||||||
|
|||||||
+26
-16
@@ -259,7 +259,6 @@ class TransmissionSession:
|
|||||||
self.startup_timeout = startup_timeout
|
self.startup_timeout = startup_timeout
|
||||||
self.shutdown_timeout = shutdown_timeout
|
self.shutdown_timeout = shutdown_timeout
|
||||||
self._cfg_dir = ""
|
self._cfg_dir = ""
|
||||||
self._watch_dir = ""
|
|
||||||
self._port = 0
|
self._port = 0
|
||||||
self._rpc = ""
|
self._rpc = ""
|
||||||
self._proc = None
|
self._proc = None
|
||||||
@@ -270,8 +269,6 @@ class TransmissionSession:
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
self._cfg_dir = tempfile.mkdtemp(prefix="hate_crack_transmission_")
|
self._cfg_dir = tempfile.mkdtemp(prefix="hate_crack_transmission_")
|
||||||
self._watch_dir = os.path.join(self._cfg_dir, "watch")
|
|
||||||
os.makedirs(self._watch_dir, exist_ok=True)
|
|
||||||
self._port = _pick_free_port()
|
self._port = _pick_free_port()
|
||||||
self._rpc = f"127.0.0.1:{self._port}"
|
self._rpc = f"127.0.0.1:{self._port}"
|
||||||
self._proc = subprocess.Popen(
|
self._proc = subprocess.Popen(
|
||||||
@@ -288,8 +285,7 @@ class TransmissionSession:
|
|||||||
"--download-dir",
|
"--download-dir",
|
||||||
self.save_dir,
|
self.save_dir,
|
||||||
"--no-portmap",
|
"--no-portmap",
|
||||||
"--watch-dir",
|
"--no-watch-dir",
|
||||||
self._watch_dir,
|
|
||||||
],
|
],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
@@ -345,22 +341,38 @@ class TransmissionSession:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add(self, torrent_path: str) -> int:
|
def add(self, torrent_path: str) -> int:
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
before_ids = {e["id"] for e in self.list()}
|
before_ids = {e["id"] for e in self.list()}
|
||||||
shutil.copy2(torrent_path, self._watch_dir)
|
result = subprocess.run(
|
||||||
deadline = time.monotonic() + 30.0
|
[
|
||||||
while time.monotonic() < deadline:
|
"transmission-remote",
|
||||||
after_entries = self.list()
|
self._rpc,
|
||||||
new_ids = [e["id"] for e in after_entries if e["id"] not in before_ids]
|
"-a",
|
||||||
if new_ids:
|
torrent_path,
|
||||||
return new_ids[0]
|
],
|
||||||
time.sleep(0.5)
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
out = result.stdout or ""
|
||||||
|
m = re.search(r"Added torrent.*\n.*ID:\s*(\d+)", out)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1))
|
||||||
|
m = re.search(r"torrent added\s*\(id\s+(\d+)\)", out, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1))
|
||||||
|
after_entries = self.list()
|
||||||
|
new_ids = [e["id"] for e in after_entries if e["id"] not in before_ids]
|
||||||
|
if new_ids:
|
||||||
|
return new_ids[0]
|
||||||
raise RuntimeError(f"Failed to add torrent: {torrent_path}")
|
raise RuntimeError(f"Failed to add torrent: {torrent_path}")
|
||||||
|
|
||||||
def list(self) -> list:
|
def list(self) -> list:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["transmission-remote", self._rpc, "--no-auth", "-l"],
|
["transmission-remote", self._rpc, "-l"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
@@ -417,7 +429,6 @@ class TransmissionSession:
|
|||||||
[
|
[
|
||||||
"transmission-remote",
|
"transmission-remote",
|
||||||
self._rpc,
|
self._rpc,
|
||||||
"--no-auth",
|
|
||||||
f"-t{torrent_id}",
|
f"-t{torrent_id}",
|
||||||
"--info-files",
|
"--info-files",
|
||||||
],
|
],
|
||||||
@@ -461,7 +472,6 @@ class TransmissionSession:
|
|||||||
[
|
[
|
||||||
"transmission-remote",
|
"transmission-remote",
|
||||||
self._rpc,
|
self._rpc,
|
||||||
"--no-auth",
|
|
||||||
f"-t{torrent_id}",
|
f"-t{torrent_id}",
|
||||||
"--remove",
|
"--remove",
|
||||||
],
|
],
|
||||||
|
|||||||
+20
-23
@@ -140,44 +140,41 @@ class TestTransmissionSession:
|
|||||||
with pytest.raises(RuntimeError, match="Transmission daemon failed"):
|
with pytest.raises(RuntimeError, match="Transmission daemon failed"):
|
||||||
ts.__enter__()
|
ts.__enter__()
|
||||||
|
|
||||||
def test_add_copies_to_watch_dir_and_returns_new_id(self, tmp_path):
|
def test_add_uses_transmission_remote_and_returns_new_id(self, tmp_path):
|
||||||
ts = TransmissionSession(str(tmp_path))
|
ts = TransmissionSession(str(tmp_path))
|
||||||
ts._rpc = "127.0.0.1:9999"
|
ts._rpc = "127.0.0.1:9999"
|
||||||
ts._watch_dir = str(tmp_path / "watch")
|
# Before: IDs 3 and 5. After add: ID 7 appears.
|
||||||
# Before: IDs 3 and 5. After first poll: ID 7 appears.
|
|
||||||
list_calls = iter([
|
list_calls = iter([
|
||||||
[{"id": 3}, {"id": 5}],
|
[{"id": 3}, {"id": 5}],
|
||||||
[{"id": 3}, {"id": 5}, {"id": 7}],
|
[{"id": 3}, {"id": 5}, {"id": 7}],
|
||||||
])
|
])
|
||||||
with patch("shutil.copy2"), patch.object(ts, "list", side_effect=list_calls), \
|
run_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
patch("time.sleep"), patch("time.monotonic", side_effect=[0.0, 1.0]):
|
with patch("subprocess.run", return_value=run_result), \
|
||||||
|
patch.object(ts, "list", side_effect=list_calls):
|
||||||
tid = ts.add("/tmp/foo.torrent")
|
tid = ts.add("/tmp/foo.torrent")
|
||||||
assert tid == 7
|
assert tid == 7
|
||||||
|
|
||||||
def test_add_polls_until_daemon_picks_up_torrent(self, tmp_path):
|
def test_add_parses_id_from_output(self, tmp_path):
|
||||||
ts = TransmissionSession(str(tmp_path))
|
ts = TransmissionSession(str(tmp_path))
|
||||||
ts._rpc = "127.0.0.1:9999"
|
ts._rpc = "127.0.0.1:9999"
|
||||||
ts._watch_dir = str(tmp_path / "watch")
|
before_list = [{"id": 1}]
|
||||||
# First two polls: no new ID. Third poll: ID 9 appears.
|
run_result = MagicMock(
|
||||||
list_calls = iter([
|
returncode=0,
|
||||||
[{"id": 1}],
|
stdout="torrent added (id 42)\n",
|
||||||
[{"id": 1}],
|
stderr="",
|
||||||
[{"id": 1}],
|
)
|
||||||
[{"id": 1}, {"id": 9}],
|
with patch("subprocess.run", return_value=run_result), \
|
||||||
])
|
patch.object(ts, "list", return_value=before_list):
|
||||||
monotonic_vals = iter([0.0, 1.0, 2.0, 3.0, 4.0])
|
|
||||||
with patch("shutil.copy2"), patch.object(ts, "list", side_effect=list_calls), \
|
|
||||||
patch("time.sleep"), patch("time.monotonic", side_effect=monotonic_vals):
|
|
||||||
tid = ts.add("/tmp/foo.torrent")
|
tid = ts.add("/tmp/foo.torrent")
|
||||||
assert tid == 9
|
assert tid == 42
|
||||||
|
|
||||||
def test_add_raises_on_timeout(self, tmp_path):
|
def test_add_raises_when_torrent_not_added(self, tmp_path):
|
||||||
ts = TransmissionSession(str(tmp_path))
|
ts = TransmissionSession(str(tmp_path))
|
||||||
ts._rpc = "127.0.0.1:9999"
|
ts._rpc = "127.0.0.1:9999"
|
||||||
ts._watch_dir = str(tmp_path / "watch")
|
# list returns the same IDs before and after; output has no ID.
|
||||||
# list never returns a new ID; monotonic jumps past the 10s deadline.
|
run_result = MagicMock(returncode=1, stdout="", stderr="error")
|
||||||
with patch("shutil.copy2"), patch.object(ts, "list", return_value=[{"id": 1}]), \
|
with patch("subprocess.run", return_value=run_result), \
|
||||||
patch("time.sleep"), patch("time.monotonic", side_effect=[0.0, 100.0]):
|
patch.object(ts, "list", return_value=[{"id": 1}]):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
ts.add("/tmp/foo.torrent")
|
ts.add("/tmp/foo.torrent")
|
||||||
|
|
||||||
|
|||||||
@@ -97,3 +97,45 @@ def test_docker_hashcat_cracks_simple_password(docker_image):
|
|||||||
assert run.returncode == 0, (
|
assert run.returncode == 0, (
|
||||||
f"Docker hashcat crack failed. stdout={run.stdout} stderr={run.stderr}"
|
f"Docker hashcat crack failed. stdout={run.stdout} stderr={run.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(300)
|
||||||
|
def test_docker_torrent_downloads_wordlists(docker_image, tmp_path):
|
||||||
|
downloads_dir = tmp_path / "downloads"
|
||||||
|
downloads_dir.mkdir()
|
||||||
|
|
||||||
|
py_cmd = (
|
||||||
|
"from hate_crack.api import fetch_torrent_metadata, run_torrent_session; "
|
||||||
|
"t1 = fetch_torrent_metadata('ignis-10K.txt'); "
|
||||||
|
"t2 = fetch_torrent_metadata('hashmob.net_2025.micro.found'); "
|
||||||
|
"files = [f for f in (t1, t2) if f]; "
|
||||||
|
"run_torrent_session(files, '/downloads')"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
run = subprocess.run(
|
||||||
|
[
|
||||||
|
"docker", "run", "--rm",
|
||||||
|
"-v", f"{downloads_dir}:/downloads",
|
||||||
|
docker_image,
|
||||||
|
"bash", "-lc", f"/workspace/.venv/bin/python -c \"{py_cmd}\"",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
pytest.fail(f"Docker torrent test timed out after {exc.timeout}s")
|
||||||
|
|
||||||
|
assert run.returncode == 0, (
|
||||||
|
f"Torrent session failed. stdout={run.stdout} stderr={run.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ignis10k = downloads_dir / "ignis-10K.txt"
|
||||||
|
micro = downloads_dir / "hashmob.net_2025.micro.found"
|
||||||
|
assert ignis10k.exists() and ignis10k.stat().st_size > 0, (
|
||||||
|
f"ignis-10K.txt missing/empty. stdout={run.stdout} stderr={run.stderr}"
|
||||||
|
)
|
||||||
|
assert micro.exists() and micro.stat().st_size > 0, (
|
||||||
|
f"hashmob.net_2025.micro.found missing/empty. stdout={run.stdout} stderr={run.stderr}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user