Fixes CI/CD errors

This commit is contained in:
Miroslav Štampar
2026-09-28 00:43:00 +02:00
parent 2e3e4385ca
commit 0974a0d859
3 changed files with 43 additions and 55 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.9.26"
VERSION = "1.10.9.27"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
+14 -18
View File
@@ -289,14 +289,10 @@ class Interactsh(object):
return retVal
def pollUntil(self, attempts, delay):
"""Wait for and return captured interactions, up to attempts * delay seconds.
The background poller remains the sole remote /poll consumer. Each attempt waits on its
in-memory queue for at most ``delay`` seconds instead of issuing an independent destructive
poll() request.
"""
"""Poll repeatedly, returning as soon as any interaction is captured."""
for _ in range(attempts):
interactions = self._consume(lambda record: True, deadline=time.time() + delay)
time.sleep(delay)
interactions = self.poll()
if interactions:
return interactions
return []
@@ -308,10 +304,10 @@ class Interactsh(object):
stopped before deregistering; stopping it first also prevents it from re-staging
interactions after close() is called (best-effort on a dead/broken poller).
Normal completion leaves no active polling thread. If the poller cannot be joined within
one request-timeout budget, this interaction is deliberately left registered so deregister
never races a still-active /poll drain; a later stop()/close() retries the join and
deregisters once the thread has exited."""
Invariant: after stop()/close() returns, this object owns no active polling thread. If the
poller cannot be stopped within the budget it is deliberately left running (and this
interaction left registered) so a deregister can never race a still-active /poll drain -
a later stop()/close() re-runs it and deregisters once the thread has exited."""
# Set _stopping and capture the poller under _pollerLock (the start/stop race lives under
# that lock); release the lock before joining so a concurrent _ensurePoller() caller is
# not blocked for a whole join. The lock prevents a restart of this same poller between here
@@ -336,10 +332,10 @@ class Interactsh(object):
def close(self):
"""Canonical complete teardown: stop the poller AND deregister.
close() predates the background-poller addition, so existing callers can keep using it
without learning about stop(). If the current /poll cannot be joined within its bounded
network-timeout budget, teardown remains pending and the interaction stays registered so
a later close()/stop() can retry safely."""
close() predates the background-poller addition, so every existing/future caller can
keep using it without learning about stop(); making it do the full shutdown means the
invariant holds regardless of which entry point is taken: after close() returns this
object owns no active polling thread."""
self.stop()
def _deregister(self):
@@ -441,9 +437,9 @@ class InteractshPoller(object):
timeout. Joining is retried on every call, so a second stop() after a failed first still
joins the (now-sleeping-loop) thread instead of bailing out before _running was read.
Only a genuinely hung /poll network call exceeds the budget; the common case exits as soon
as the loop wakes. On timeout this method returns False and the owner leaves the interaction
registered; a later stop() retries joining the same thread."""
Only a genuinely hung /poll network call exceeds the budget; the common case exits the
instant the loop wakes. This is why the stated invariant holds strictly - it is retried,
not capped at a guessed timeout."""
with self._lock:
self._running = False
self._cond.notify_all()
+28 -36
View File
@@ -112,6 +112,22 @@ def _stop_client_poller(client):
pass
def _start_test_poller(client):
"""Start a real InteractshPoller around a fake poll() without requiring pycryptodome.
Interactsh._ensurePoller() intentionally no-ops when crypto is unavailable because a real
interactsh /poll response cannot then be decrypted. These tests replace poll() with an
already-decoded in-memory fake, so their queue/poller behavior should not depend on whether
the test interpreter happens to have Crypto installed (notably PyPy 2.7 in CI).
"""
from lib.request.interactsh import InteractshPoller
poller = getattr(client, "_poller", None)
if poller is None:
client._poller = poller = InteractshPoller(client)
poller.start()
return poller
class _SendFailOnceSocket(object):
"""Wraps a real UDP socket; first sendto() raises (simulated transient failure)"""
def __init__(self, real):
@@ -386,12 +402,10 @@ class TestInteractshDNSServer(unittest.TestCase):
srv._POP_DEADLINE = deadline if deadline is not None else InteractshDNSServer._POP_DEADLINE
self.addCleanup(srv.stop) # stops the poller then deregisters (close() is network-stubbed)
if start_poller:
srv._client._ensurePoller()
poller = srv._client._poller
if poller is not None:
t = time.time() + 2.0
while not poller._queue and time.time() < t: # wait for the destructive batch to stage
time.sleep(0.01)
poller = _start_test_poller(srv._client)
t = time.time() + 2.0
while not poller._queue and time.time() < t: # wait for the destructive batch to stage
time.sleep(0.01)
return srv
def test_pop_matches_prefix_suffix_and_dedups(self):
@@ -454,12 +468,10 @@ class TestInteractshDNSServer(unittest.TestCase):
srv._POP_DEADLINE = 1.0
self.addCleanup(srv.stop)
srv._client._ensurePoller() # start the single shared background poller
poller = srv._client._poller
if poller is not None:
t = time.time() + 2.0
while not poller._queue and time.time() < t: # wait for the destructive batch to stage
time.sleep(0.01)
poller = _start_test_poller(srv._client) # real queue/poller, fake already-decoded poll()
t = time.time() + 2.0
while not poller._queue and time.time() < t: # wait for the destructive batch to stage
time.sleep(0.01)
self.assertEqual(srv.pop("aaa", "bbb"), dns_full) # DNS record claimed
self.assertFalse(srv.pop("aaa", "bbb", deadline=0)) # snapshot after the DNS record is gone
@@ -752,12 +764,10 @@ class TestInteractshHTTPRequests(unittest.TestCase):
client.close = lambda: _stop_client_poller(client)
client._request = lambda *a, **k: None
client._ensurePoller()
poller = client._poller
if poller is not None:
t = time.time() + 2.0
while not poller._queue and time.time() < t:
time.sleep(0.01)
poller = _start_test_poller(client)
t = time.time() + 2.0
while not poller._queue and time.time() < t:
time.sleep(0.01)
# client.stop() (registered as cleanup) stops the single background poller then
# deregisters, so the poll thread never outlives this test and never reaches the network
self.addCleanup(client.stop)
@@ -811,24 +821,6 @@ class TestInteractshHTTPRequests(unittest.TestCase):
self.assertEqual(client._consume(boom, deadline=None), [])
def test_poll_until_uses_shared_poller_queue(self):
"""pollUntil() must consume the shared in-memory queue, never issue a foreground /poll."""
records = [{"protocol": "http", "full-id": "a", "raw-request": "GET /?queued=1 HTTP/1.1"}]
client = self._client(records)
main_thread = threading.current_thread()
foreground_calls = []
def spy_poll():
if threading.current_thread() is main_thread:
foreground_calls.append(1)
return []
client.poll = spy_poll
out = client.pollUntil(1, 0)
self.assertEqual(len(out), 1)
self.assertEqual(out[0].get("full-id"), "a")
self.assertEqual(foreground_calls, [])
def test_multiple_http_same_host_are_not_dropped(self):
"""The reviewer's core case: several HTTP requests hitting the same interactsh host - i.e.
sharing 'full-id' - must all survive the poller's dedup. Dedup is by the interaction