mirror of
https://github.com/searxng/searxng.git
synced 2026-09-23 06:36:12 +00:00
[feat] utils.py: add altcha captcha solver
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Utility functions for the engines"""
|
"""Utility functions for the engines"""
|
||||||
|
|
||||||
|
from hashlib import pbkdf2_hmac
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -811,3 +812,34 @@ def format_duration(duration: str | int) -> str:
|
|||||||
if length.tm_hour:
|
if length.tm_hour:
|
||||||
return time.strftime("%H:%M:%S", length)
|
return time.strftime("%H:%M:%S", length)
|
||||||
return time.strftime("%M:%S", length)
|
return time.strftime("%M:%S", length)
|
||||||
|
|
||||||
|
|
||||||
|
def _array_startswith(arr: bytes, prefix: bytes) -> bool:
|
||||||
|
return arr[: len(prefix)] == prefix
|
||||||
|
|
||||||
|
|
||||||
|
def solve_altcha(parameters: dict[str, t.Any], maxCounter: int = 1000) -> tuple[str, int] | None:
|
||||||
|
"""Solves Altcha CAPTCHAs. It derives keys using PBKDF2 until the derived
|
||||||
|
key starts with the ``keyPrefix``` from the challenge. If the solver does not
|
||||||
|
finish after ``maxCounter`` iterations, returns ``None``."""
|
||||||
|
|
||||||
|
nonce = bytes.fromhex(parameters["nonce"])
|
||||||
|
salt = bytes.fromhex(parameters["salt"])
|
||||||
|
keyPrefix = bytes.fromhex(parameters["keyPrefix"])
|
||||||
|
cost = parameters["cost"]
|
||||||
|
keyLength = parameters["keyLength"]
|
||||||
|
# e.g. "PBKDF2/SHA-256" -> "sha256"
|
||||||
|
hashAlgorithm = parameters["algorithm"].split("/")[-1].replace("-", "").lower()
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
while counter < maxCounter:
|
||||||
|
# Appends the counter to the nonce, i.e. writes the counter as a big-endian 32-bit integer.
|
||||||
|
secret = nonce + counter.to_bytes(length=4)
|
||||||
|
|
||||||
|
key = pbkdf2_hmac(hash_name=hashAlgorithm, password=secret, salt=salt, iterations=cost, dklen=keyLength)
|
||||||
|
if _array_startswith(key, keyPrefix):
|
||||||
|
return key.hex(), counter
|
||||||
|
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ def random_string(length, choices=string.ascii_letters):
|
|||||||
|
|
||||||
|
|
||||||
class TestUtils(SearxTestCase):
|
class TestUtils(SearxTestCase):
|
||||||
|
|
||||||
def test_gen_useragent(self):
|
def test_gen_useragent(self):
|
||||||
self.assertIsInstance(utils.gen_useragent(), str)
|
self.assertIsInstance(utils.gen_useragent(), str)
|
||||||
self.assertIsNotNone(utils.gen_useragent())
|
self.assertIsNotNone(utils.gen_useragent())
|
||||||
@@ -116,7 +115,6 @@ class TestUtils(SearxTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestXPathUtils(SearxTestCase): # pylint: disable=missing-class-docstring
|
class TestXPathUtils(SearxTestCase): # pylint: disable=missing-class-docstring
|
||||||
|
|
||||||
TEST_DOC = """<ul>
|
TEST_DOC = """<ul>
|
||||||
<li>Text in <b>bold</b> and <i>italic</i> </li>
|
<li>Text in <b>bold</b> and <i>italic</i> </li>
|
||||||
<li>Another <b>text</b> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="></li>
|
<li>Another <b>text</b> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="></li>
|
||||||
@@ -194,3 +192,25 @@ class TestXPathUtils(SearxTestCase): # pylint: disable=missing-class-docstring
|
|||||||
with self.assertRaises(SearxEngineXPathException) as context:
|
with self.assertRaises(SearxEngineXPathException) as context:
|
||||||
utils.eval_xpath_getindex(doc, 'count(//i)', 1)
|
utils.eval_xpath_getindex(doc, 'count(//i)', 1)
|
||||||
self.assertEqual(context.exception.message, 'the result is not a list')
|
self.assertEqual(context.exception.message, 'the result is not a list')
|
||||||
|
|
||||||
|
def test_altcha_solver(self):
|
||||||
|
# copied from a real Mojeek challenge
|
||||||
|
challenge = {
|
||||||
|
"parameters": {
|
||||||
|
"algorithm": "PBKDF2/SHA-256",
|
||||||
|
"cost": 8000,
|
||||||
|
"keyLength": 32,
|
||||||
|
"keyPrefix": "71087e0b10d819181fc5463588a230b5",
|
||||||
|
"nonce": "1449f02d6089fc29dac33c44aa0630cd",
|
||||||
|
"salt": "860b25b9ee8c123e9ad20f24d9752c9e",
|
||||||
|
"keySignature": "9945d6f881232806edf82a953e1742843b30cf9fe9d56054e1e17a9f47435aa6",
|
||||||
|
"expiresAt": 1789121497,
|
||||||
|
},
|
||||||
|
"signature": "aecf8725f019643766920b67c7e21bc383d8ff166a8e1137c6b5f4d6d19d00c1",
|
||||||
|
}
|
||||||
|
solution = utils.solve_altcha(challenge["parameters"]) # pyright: ignore[reportArgumentType]
|
||||||
|
self.assertIsNotNone(solution)
|
||||||
|
|
||||||
|
key, counter = solution # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
self.assertEqual(key, "71087e0b10d819181fc5463588a230b5744e6b9f19b86667c71ff73a9c7567aa")
|
||||||
|
self.assertEqual(counter, 260)
|
||||||
|
|||||||
Reference in New Issue
Block a user