From c07bab1a2ba003da0219f1e98d3791128667166f Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Thu, 18 Jun 2026 17:56:48 -0400 Subject: [PATCH] fix(hashview): degrade to direct hashfile-ID entry when listing API absent The hashfile-listing route (/v1/hashfiles/hash_type/) only exists on Hashview builds from 2026-06-08+ (v0.8.3-dev). On `main` and older servers it 404s, which surfaced as "Could not list hashfiles" and a dead end. The download flow is now version-adaptive: - get_all_customer_hashfiles stops sweeping after the first 404 (the listing endpoint is absent) instead of probing every common type. - The menu treats listing as best-effort: when no listing is available it prints a clear note and accepts a hashfile ID typed directly (read from the web UI), with no list-membership check. The chosen file's type is resolved via GET /v1/getHashType/ and downloaded via GET /v1/hashfiles/ -- both present on all server versions. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- hate_crack/api.py | 12 +++++++ hate_crack/main.py | 77 ++++++++++++++++++------------------------ tests/test_hashview.py | 16 +++++++++ 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index b086e87..22d83a1 100644 --- a/README.md +++ b/README.md @@ -913,7 +913,7 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi ### Version History Version 2.10.6 - - Fixed the Hashview integration calling API routes that don't exist in Hashview (verified against v0.8.3-dev), which 404'd as soon as a customer ID was entered ("Could not list hashfiles"). The customer→hashfile listing relied on a phantom `GET /v1/hashfiles` list-all route; it now enumerates via the real `GET /v1/hashfiles/hash_type/` endpoint. Since that route lists per hash type, the download flow sweeps common hashcat modes to display all of a customer's uploaded hashfiles (falling back to a manual hash-type prompt only if none are found among the common set), then filters by customer. Additional client-side route fixes: hashfile hash-type lookup now uses `GET /v1/getHashType/`; "left" (uncracked) hash download uses `GET /v1/hashfiles/`; `delete_job` uses `DELETE /v1/jobs/`; `start_job` uses `POST`. Hashview exposes no stop-job route, so `stop_job` now raises with guidance to use `delete_job`; and no bulk cracked-hash export exists, so the best-effort "found" merge degrades gracefully + - Fixed the Hashview integration calling API routes that don't exist in Hashview (verified against v0.8.3-dev), which 404'd as soon as a customer ID was entered ("Could not list hashfiles"). The customer→hashfile listing relied on a phantom `GET /v1/hashfiles` list-all route; it now enumerates via the real `GET /v1/hashfiles/hash_type/` endpoint where available — the download flow sweeps common hashcat modes to display a customer's uploaded hashfiles. That listing route only exists on Hashview builds from 2026-06-08+ (the `v0.8.3-dev` branch); on `main`/older servers there is no hashfile-listing API at all, so the flow now degrades gracefully to entering the hashfile ID directly (looked up in the Hashview web UI) and resolving its type via `GET /v1/getHashType/`. Additional client-side route fixes: hashfile hash-type lookup now uses `GET /v1/getHashType/`; "left" (uncracked) hash download uses `GET /v1/hashfiles/`; `delete_job` uses `DELETE /v1/jobs/`; `start_job` uses `POST`. Hashview exposes no stop-job route, so `stop_job` now raises with guidance to use `delete_job`; and no bulk cracked-hash export exists, so the best-effort "found" merge degrades gracefully Version 2.10.5 - Pipal analysis no longer corrupts its input when cracked passwords contain `$HEX[...]` rows. `binascii.unhexlify().decode()` returned the bytes without the trailing newline that normal rows inherit from `password[-1]`, so every HEX-encoded password got concatenated with the next one in the `.passwords` file fed to pipal (e.g. three cracks → two lines, one of them a bogus mashup). Pipal then under-counted entries and reported wrong top base words. The HEX branch now re-appends `\n` so each cracked password lands on its own line diff --git a/hate_crack/api.py b/hate_crack/api.py index c0eec96..83d8120 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1261,6 +1261,18 @@ class HashviewAPI: try: files = self.get_hashfiles_by_type(ht) except Exception as e: + status = getattr(getattr(e, "response", None), "status_code", None) + if status == 404: + # The /v1/hashfiles/hash_type route doesn't exist on this + # server (e.g. Hashview main, or builds before 2026-06-08), + # so no per-type sweep is possible. Stop after the first 404 + # instead of hammering the server with one request per type. + if self.debug: + print( + "[DEBUG] get_all_customer_hashfiles: hash_type listing " + "endpoint absent (404); aborting sweep" + ) + break if self.debug: print(f"[DEBUG] get_all_customer_hashfiles: type {ht} failed: {e}") continue diff --git a/hate_crack/main.py b/hate_crack/main.py index bed09ac..3f5e5dd 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -3758,55 +3758,31 @@ def hashview_api(): ) continue - # List hashfiles for the customer. Hashview has no - # list-all route, so we enumerate by sweeping the - # per-type endpoint across common hashcat modes and - # showing whatever the customer has uploaded. + # Try to list the customer's hashfiles for convenience. + # This only works on Hashview servers that expose the + # /v1/hashfiles/hash_type endpoint (v0.8.3-dev, added + # 2026-06-08); on `main` or older builds there is no + # hashfile-listing API, so we fall back to entering the + # hashfile ID directly (look it up in the web UI). + hashfile_map = {} try: print( "\nScanning customer hashfiles across common hash types..." ) - customer_hashfiles = api_harness.get_all_customer_hashfiles( - customer_id + customer_hashfiles = ( + api_harness.get_all_customer_hashfiles(customer_id) ) + except Exception as e: + customer_hashfiles = [] + if debug_mode: + print(f"[DEBUG] hashfile listing unavailable: {e}") - # Fallback: nothing among common types. The file may - # use an uncommon mode, so let the user name it. - if not customer_hashfiles: - print( - f"\nNo hashfiles found for customer ID {customer_id} " - "among common hash types." - ) - ht_input = input( - "Enter a specific hashcat hash-type to check " - "(e.g. 1000), or Q to cancel: " - ).strip() - if ht_input.lower() == "q": - cancel_download = True - break - if not ht_input.isdigit(): - print( - "\n✗ Error: Hash type must be a numeric hashcat mode." - ) - continue - customer_hashfiles = ( - api_harness.get_customer_hashfiles( - customer_id, hash_type=ht_input - ) - ) - - if not customer_hashfiles: - print( - f"\nNo hashfiles found for customer ID {customer_id}" - ) - continue - + if customer_hashfiles: print("\n" + "=" * 120) print(f"Hashfiles for Customer ID {customer_id}:") print("=" * 120) print(f"{'ID':<10} {'Hash Type':<10} {'Name':<96}") print("-" * 120) - hashfile_map = {} for hf in customer_hashfiles: hf_id = hf.get("id") hf_name = hf.get("name", "N/A") @@ -3826,22 +3802,33 @@ def hashview_api(): hashfile_map[int(hf_id)] = hf_type print("=" * 120) print(f"Total: {len(hashfile_map)} hashfile(s)") - except Exception as e: - print(f"\nWarning: Could not list hashfiles: {e}") - continue + else: + print( + "\nThis Hashview server has no hashfile-listing API " + "(it lacks the /v1/hashfiles/hash_type endpoint), or " + f"customer {customer_id} has no hashfiles of a common " + "type. Look up the hashfile ID in the Hashview web UI " + "and enter it below." + ) while True: + hashfile_id_input = input( + "\nEnter hashfile ID (or Q to cancel): " + ).strip() + if hashfile_id_input.lower() == "q": + cancel_download = True + break try: - hashfile_id_input = input( - "\nEnter hashfile ID: " - ).strip() hashfile_id = int(hashfile_id_input) except ValueError: print( "\n✗ Error: Invalid ID entered. Please enter a numeric ID." ) continue - if hashfile_id not in hashfile_map: + # Only restrict to the listed set when we actually + # have a listing; otherwise accept any ID the user + # read from the web UI. + if hashfile_map and hashfile_id not in hashfile_map: print( "\n✗ Error: Hashfile ID not in the list. Please try again." ) diff --git a/tests/test_hashview.py b/tests/test_hashview.py index 1e092b4..9a140d3 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -118,6 +118,22 @@ class TestHashviewAPI: assert str(by_id[1]["hash_type"]) == "1000" # first type seen wins assert str(by_id[3]["hash_type"]) == "5600" + def test_get_all_customer_hashfiles_aborts_on_404(self, api): + """A 404 means the listing endpoint is absent (e.g. Hashview main); + the sweep stops after the first request instead of probing every type.""" + import requests + + def _raise_404(ht): + resp = Mock() + resp.status_code = 404 + raise requests.exceptions.HTTPError("404 Not Found", response=resp) + + api.get_hashfiles_by_type = Mock(side_effect=_raise_404) + result = api.get_all_customer_hashfiles(1, hash_types=[1000, 5600, 3000]) + assert result == [] + # Stopped after the first 404, did not sweep all three types. + assert api.get_hashfiles_by_type.call_count == 1 + def test_get_all_customer_hashfiles_skips_failing_types(self, api): """A per-type query that errors is skipped, not fatal."""