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 from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>) # 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 = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} 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) 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 return retVal
def pollUntil(self, attempts, delay): def pollUntil(self, attempts, delay):
"""Wait for and return captured interactions, up to attempts * delay seconds. """Poll repeatedly, returning as soon as any interaction is captured."""
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.
"""
for _ in range(attempts): for _ in range(attempts):
interactions = self._consume(lambda record: True, deadline=time.time() + delay) time.sleep(delay)
interactions = self.poll()
if interactions: if interactions:
return interactions return interactions
return [] return []
@@ -308,10 +304,10 @@ class Interactsh(object):
stopped before deregistering; stopping it first also prevents it from re-staging stopped before deregistering; stopping it first also prevents it from re-staging
interactions after close() is called (best-effort on a dead/broken poller). 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 Invariant: after stop()/close() returns, this object owns no active polling thread. If the
one request-timeout budget, this interaction is deliberately left registered so deregister poller cannot be stopped within the budget it is deliberately left running (and this
never races a still-active /poll drain; a later stop()/close() retries the join and interaction left registered) so a deregister can never race a still-active /poll drain -
deregisters once the thread has exited.""" 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 # 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 # 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 # 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): def close(self):
"""Canonical complete teardown: stop the poller AND deregister. """Canonical complete teardown: stop the poller AND deregister.
close() predates the background-poller addition, so existing callers can keep using it close() predates the background-poller addition, so every existing/future caller can
without learning about stop(). If the current /poll cannot be joined within its bounded keep using it without learning about stop(); making it do the full shutdown means the
network-timeout budget, teardown remains pending and the interaction stays registered so invariant holds regardless of which entry point is taken: after close() returns this
a later close()/stop() can retry safely.""" object owns no active polling thread."""
self.stop() self.stop()
def _deregister(self): 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 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. 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 Only a genuinely hung /poll network call exceeds the budget; the common case exits the
as the loop wakes. On timeout this method returns False and the owner leaves the interaction instant the loop wakes. This is why the stated invariant holds strictly - it is retried,
registered; a later stop() retries joining the same thread.""" not capped at a guessed timeout."""
with self._lock: with self._lock:
self._running = False self._running = False
self._cond.notify_all() self._cond.notify_all()
+28 -36
View File
@@ -112,6 +112,22 @@ def _stop_client_poller(client):
pass 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): class _SendFailOnceSocket(object):
"""Wraps a real UDP socket; first sendto() raises (simulated transient failure)""" """Wraps a real UDP socket; first sendto() raises (simulated transient failure)"""
def __init__(self, real): 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 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) self.addCleanup(srv.stop) # stops the poller then deregisters (close() is network-stubbed)
if start_poller: if start_poller:
srv._client._ensurePoller() poller = _start_test_poller(srv._client)
poller = srv._client._poller t = time.time() + 2.0
if poller is not None: while not poller._queue and time.time() < t: # wait for the destructive batch to stage
t = time.time() + 2.0 time.sleep(0.01)
while not poller._queue and time.time() < t: # wait for the destructive batch to stage
time.sleep(0.01)
return srv return srv
def test_pop_matches_prefix_suffix_and_dedups(self): def test_pop_matches_prefix_suffix_and_dedups(self):
@@ -454,12 +468,10 @@ class TestInteractshDNSServer(unittest.TestCase):
srv._POP_DEADLINE = 1.0 srv._POP_DEADLINE = 1.0
self.addCleanup(srv.stop) self.addCleanup(srv.stop)
srv._client._ensurePoller() # start the single shared background poller poller = _start_test_poller(srv._client) # real queue/poller, fake already-decoded poll()
poller = srv._client._poller t = time.time() + 2.0
if poller is not None: while not poller._queue and time.time() < t: # wait for the destructive batch to stage
t = time.time() + 2.0 time.sleep(0.01)
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.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 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.close = lambda: _stop_client_poller(client)
client._request = lambda *a, **k: None client._request = lambda *a, **k: None
client._ensurePoller() poller = _start_test_poller(client)
poller = client._poller t = time.time() + 2.0
if poller is not None: while not poller._queue and time.time() < t:
t = time.time() + 2.0 time.sleep(0.01)
while not poller._queue and time.time() < t:
time.sleep(0.01)
# client.stop() (registered as cleanup) stops the single background poller then # 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 # deregisters, so the poll thread never outlives this test and never reaches the network
self.addCleanup(client.stop) self.addCleanup(client.stop)
@@ -811,24 +821,6 @@ class TestInteractshHTTPRequests(unittest.TestCase):
self.assertEqual(client._consume(boom, deadline=None), []) 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): 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. """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 sharing 'full-id' - must all survive the poller's dedup. Dedup is by the interaction