fix(llm): only prepend http:// to OLLAMA_HOST when no scheme is present

ollamaUrl was built as "http://" + OLLAMA_HOST, so any value carrying a scheme
- the form Ollama's own tooling accepts - was mangled:

    OLLAMA_HOST=https://ollama.example.com
    -> http://https://ollama.example.com

The LLM attack then could not connect, and reaching a remote Ollama over TLS
was impossible. llm.py derives its client base URL as f"{ollamaUrl}/v1", so the
malformation propagated into the OpenAI-compatible client.

Extract _normalize_ollama_url() so the logic is unit-testable - ollamaUrl is
assigned at module import, which is otherwise only reachable via reload - and
prepend the scheme only when absent. Also strip trailing slashes, since callers
append paths. The bare host:port default is unchanged.

Fixes #119

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-07-24 19:19:38 -04:00
co-authored by Claude
parent 9459c21b14
commit 2376ac5c10
3 changed files with 105 additions and 1 deletions
+12
View File
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are omitted for releases predating this file; see the git tags for exact timing.
## [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
+18 -1
View File
@@ -361,6 +361,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)."""
@@ -450,7 +467,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))
+75
View File
@@ -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)