diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac4799..0c8f972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,18 @@ Dates are omitted for releases predating this file; see the git tags for exact t defaults, and the process returns a clean exit code (0 on success, non-zero on a bad hash file, hash type, wordlist, or rule name). +## [2.13.1] - 2026-07-24 + +### Fixed + +- **`OLLAMA_HOST` values that include a scheme no longer produce a malformed URL** + (issue #119). The Ollama base URL was built as `"http://" + OLLAMA_HOST`, so a value in + the form Ollama's own tooling accepts — `http://box:11434` or + `https://ollama.example.com` — became `http://http://box:11434` and the LLM attack could + not connect. `http://` is now prepended only when no scheme is present, and trailing + slashes are stripped because callers append paths (`f"{ollamaUrl}/v1"`). Reaching a remote + Ollama over TLS works as a result. The bare `host:port` default is unchanged. + ## [2.13.0] - 2026-07-24 ### Added diff --git a/hate_crack/main.py b/hate_crack/main.py index 0dda836..6018b51 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -362,6 +362,23 @@ else: hcatPotfilePath = os.path.join(hate_path, hcatPotfilePath) +def _normalize_ollama_url(host: str) -> str: + """Turn an ``OLLAMA_HOST`` value into a usable base URL. + + Ollama's own tooling accepts both a bare ``host:port`` and a full URL, so + accept either. Only prepend ``http://`` when no scheme is present; + unconditionally prepending it produced URLs like + ``http://https://ollama.example.com``. Trailing slashes are stripped + because callers append paths (``f"{ollamaUrl}/v1"``). + """ + host = (host or "").strip() + if not host: + return "http://localhost:11434" + if "://" not in host: + host = "http://" + host + return host.rstrip("/") + + def _maybe_append_username_flag(cmd): """Append --username if the active hash file has user:hash format and the flag isn't already present (from hcatTuning or elsewhere).""" @@ -451,7 +468,7 @@ hcatGoodMeasureBaseList = config_parser["hcatGoodMeasureBaseList"] hcatDebugLogPath = os.path.expanduser(config_parser["hcatDebugLogPath"]) -ollamaUrl = "http://" + os.environ.get("OLLAMA_HOST", "localhost:11434") +ollamaUrl = _normalize_ollama_url(os.environ.get("OLLAMA_HOST", "localhost:11434")) ollamaModel = config_parser.get("ollamaModel", "qwen2.5:32b") ollamaNumCtx = int(config_parser.get("ollamaNumCtx", 2048)) ollamaTimeout = float(config_parser.get("ollamaTimeout", 300)) diff --git a/tests/test_ollama_url_normalization.py b/tests/test_ollama_url_normalization.py new file mode 100644 index 0000000..845f116 --- /dev/null +++ b/tests/test_ollama_url_normalization.py @@ -0,0 +1,75 @@ +"""Tests for OLLAMA_HOST -> ollamaUrl normalization (issue #119). + +``ollamaUrl`` used to be built as ``"http://" + OLLAMA_HOST``, which mangled any +value that already carried a scheme -- the form Ollama's own tooling accepts. +""" + +import importlib +import os +from unittest import mock + +import pytest + +from hate_crack import main as hc_main + + +@pytest.mark.parametrize( + ("host", "expected"), + [ + # Bare host:port -- the historical default, must keep working. + ("localhost:11434", "http://localhost:11434"), + ("box:11434", "http://box:11434"), + ("127.0.0.1:11434", "http://127.0.0.1:11434"), + # Scheme already present -- previously produced http://http://... + ("http://box:11434", "http://box:11434"), + ("https://ollama.example.com", "https://ollama.example.com"), + ("https://ollama.example.com:443", "https://ollama.example.com:443"), + # Trailing slashes stripped: callers append f"{ollamaUrl}/v1". + ("http://box:11434/", "http://box:11434"), + ("https://ollama.example.com///", "https://ollama.example.com"), + ("box:11434/", "http://box:11434"), + # Whitespace and empty values fall back to the default. + (" box:11434 ", "http://box:11434"), + ("", "http://localhost:11434"), + (" ", "http://localhost:11434"), + ], +) +def test_normalize_ollama_url(host, expected): + assert hc_main._normalize_ollama_url(host) == expected + + +def test_scheme_is_never_doubled(): + """The exact regression from issue #119.""" + for host in ("http://box:11434", "https://ollama.example.com"): + assert "http://http" not in hc_main._normalize_ollama_url(host) + assert "http://https" not in hc_main._normalize_ollama_url(host) + + +def test_result_builds_a_valid_v1_endpoint(): + """llm.py derives its client base URL as f"{ollamaUrl}/v1".""" + for host in ("box:11434", "http://box:11434/", "https://x.example.com//"): + url = hc_main._normalize_ollama_url(host) + assert f"{url}/v1".count("//") == 1 + + +def test_module_level_ollamaUrl_honours_a_scheme_in_the_env(): + """Guard the wiring, not just the helper: ollamaUrl is built at import.""" + with mock.patch.dict( + os.environ, {"OLLAMA_HOST": "https://ollama.example.com"}, clear=False + ): + reloaded = importlib.reload(hc_main) + try: + assert reloaded.ollamaUrl == "https://ollama.example.com" + finally: + # Restore the module to the ambient environment for other tests. + importlib.reload(reloaded) + + +def test_module_level_ollamaUrl_defaults_to_localhost(): + env = {k: v for k, v in os.environ.items() if k != "OLLAMA_HOST"} + with mock.patch.dict(os.environ, env, clear=True): + reloaded = importlib.reload(hc_main) + try: + assert reloaded.ollamaUrl == "http://localhost:11434" + finally: + importlib.reload(reloaded)