5555
5656
5757def _sigint_handler (signum , frame ):
58+ # NOTE: sys.stderr.write() is async-signal-safe; console.print() is NOT
59+ # (Rich holds a threading.Lock internally — calling it from a signal handler
60+ # while the main thread already holds that lock causes a deadlock, which is
61+ # more likely on Android/Termux due to its scheduler).
5862 global _last_interrupt_time , _skip_current , _exit_requested
5963 now = time .monotonic ()
6064 if now - _last_interrupt_time < 1.5 :
6165 _exit_requested = True
62- console .print ("\n [bold red][!!] Double Ctrl+C detected — exiting...[/bold red]" )
66+ sys .stderr .write ("\n [!!] Double Ctrl+C detected — exiting...\n " )
67+ sys .stderr .flush ()
6368 else :
6469 _skip_current = True
65- console .print ("\n [yellow][~] Ctrl+C — skipping current task...[/yellow]" )
70+ sys .stderr .write ("\n [~] Ctrl+C — skipping current task...\n " )
71+ sys .stderr .flush ()
6672 _last_interrupt_time = now
6773
6874
@@ -533,13 +539,15 @@ def _measure_baseline_latency(self, url: str) -> Optional[float]:
533539 for _ in range (_BASELINE_SAMPLES ):
534540 if self .circuit_breaker .is_dead (url ):
535541 return None
542+ if _exit_requested or _skip_current :
543+ return None
536544 t0 = time .monotonic ()
537545 response = self ._get (url )
538546 elapsed = time .monotonic () - t0
539547 if response is None :
540548 return None
541549 times .append (elapsed )
542- time . sleep (0.3 )
550+ _interruptible_sleep (0.3 )
543551 baseline = sum (times ) / len (times )
544552 if baseline > _MAX_BASELINE_S :
545553 return None
@@ -588,14 +596,16 @@ def _probe_parameter(self, url: str, param_name: str, baseline_content: str) ->
588596 baseline_status : Optional [int ] = None
589597
590598 for i in range (_PROBE_SAMPLES ):
599+ if _exit_requested or _skip_current :
600+ return False
591601 r = self ._get (url )
592602 if r is None :
593603 return False
594604 if i == 0 :
595605 baseline_status = r .status_code
596606 sim = difflib .SequenceMatcher (None , baseline_content , r .text ).ratio ()
597607 noise_samples .append (1.0 - sim )
598- time . sleep (0.2 )
608+ _interruptible_sleep (0.2 )
599609
600610 if not noise_samples :
601611 return False
@@ -636,6 +646,8 @@ def _test_error_based(self, url: str, param_name: str) -> Dict:
636646 for payload in payloads :
637647 if self .circuit_breaker .is_dead (url ):
638648 break
649+ if _exit_requested or _skip_current :
650+ break
639651 test_url = self ._inject_payload (url , param_name , payload )
640652 response = self ._get (test_url )
641653 if response is None :
@@ -652,7 +664,7 @@ def _test_error_based(self, url: str, param_name: str) -> Dict:
652664 return result
653665
654666 if self .stealth :
655- time . sleep (random .uniform (1.5 , 3 ))
667+ _interruptible_sleep (random .uniform (1.5 , 3 ))
656668
657669 return result
658670
@@ -677,22 +689,26 @@ def _test_boolean_blind(self, url: str, param_name: str, baseline_len: int) -> D
677689 for payload , payload_type in bool_payloads :
678690 if self .circuit_breaker .is_dead (url ):
679691 break
692+ if _exit_requested or _skip_current :
693+ break
680694
681695 test_url = self ._inject_payload (url , param_name , payload )
682696 samples : List [int ] = []
683697
684698 for _ in range (_BOOL_SAMPLES ):
699+ if _exit_requested or _skip_current :
700+ break
685701 r = self ._get (test_url )
686702 if r is not None :
687703 samples .append (len (r .text ))
688704 if self .stealth :
689- time . sleep (random .uniform (0.5 , 1 ))
705+ _interruptible_sleep (random .uniform (0.5 , 1 ))
690706
691707 if len (samples ) >= 2 :
692708 (true_groups if payload_type == "true" else false_groups ).append (samples )
693709
694710 if self .stealth :
695- time . sleep (random .uniform (1 , 2 ))
711+ _interruptible_sleep (random .uniform (1 , 2 ))
696712
697713 if not true_groups or not false_groups :
698714 return result
@@ -771,6 +787,8 @@ def _test_time_based_blind(self, url: str, param_name: str) -> Dict:
771787 for _ in range (_TIMEBASED_CONFIRM ):
772788 if self .circuit_breaker .is_dead (url ):
773789 break
790+ if _exit_requested or _skip_current :
791+ break
774792 t_c = time .monotonic ()
775793 r_c = self ._get (neutral_url )
776794 elapsed_c = time .monotonic () - t_c
@@ -780,7 +798,7 @@ def _test_time_based_blind(self, url: str, param_name: str) -> Dict:
780798 else :
781799 confirm_times .append (elapsed_c )
782800
783- time . sleep (0.5 )
801+ _interruptible_sleep (0.5 )
784802
785803 if confirm_failures > 0 :
786804 continue
@@ -841,6 +859,8 @@ def test_sqli(self, url: str) -> Dict:
841859 if self .circuit_breaker .is_dead (url ):
842860 result ["message" ] = "Host became unreachable during testing"
843861 break
862+ if _exit_requested or _skip_current :
863+ break
844864
845865 if not self ._probe_parameter (url , param_name , baseline_content ):
846866 continue
@@ -870,7 +890,7 @@ def test_sqli(self, url: str) -> Dict:
870890 )
871891
872892 if self .stealth :
873- time . sleep (random .uniform (2 , 4 ))
893+ _interruptible_sleep (random .uniform (2 , 4 ))
874894
875895 if confidence_scores :
876896 avg = sum (confidence_scores ) / len (confidence_scores )
@@ -905,6 +925,8 @@ def test_post_sqli(self, url: str, post_data: Dict[str, str]) -> Dict:
905925 for param_name in post_data .keys ():
906926 if self .circuit_breaker .is_dead (url ):
907927 break
928+ if _exit_requested or _skip_current :
929+ break
908930
909931 payload_dict = post_data .copy ()
910932 payload_dict [param_name ] = str (post_data [param_name ]) + "'"
@@ -938,7 +960,7 @@ def test_post_sqli(self, url: str, post_data: Dict[str, str]) -> Dict:
938960 pass
939961
940962 if self .stealth :
941- time . sleep (random .uniform (2 , 4 ))
963+ _interruptible_sleep (random .uniform (2 , 4 ))
942964
943965 if confidence_scores :
944966 avg = sum (confidence_scores ) / len (confidence_scores )
@@ -966,6 +988,8 @@ def test_json_sqli(self, url: str, json_data: Dict[str, str]) -> Dict:
966988 for key in json_data .keys ():
967989 if self .circuit_breaker .is_dead (url ):
968990 break
991+ if _exit_requested or _skip_current :
992+ break
969993
970994 payload_dict = json_data .copy ()
971995 payload_dict [key ] = payload_dict [key ] + "'"
@@ -997,7 +1021,7 @@ def test_json_sqli(self, url: str, json_data: Dict[str, str]) -> Dict:
9971021 pass
9981022
9991023 if self .stealth :
1000- time . sleep (random .uniform (2 , 4 ))
1024+ _interruptible_sleep (random .uniform (2 , 4 ))
10011025
10021026 if confidence_scores :
10031027 avg = sum (confidence_scores ) / len (confidence_scores )
@@ -1177,7 +1201,10 @@ def __init__(self, config: Dict, output_file: str = None):
11771201 self ._total_results_at_last_extended_delay : int = 0
11781202
11791203 def _hash_url (self , url : str ) -> str :
1180- return hashlib .md5 (url .encode ()).hexdigest ()
1204+ # usedforsecurity=False: required on Python 3.9+ when OpenSSL runs in
1205+ # FIPS mode (some custom Android ROMs enforce this) — md5 is not used
1206+ # for any cryptographic purpose here, only as a fast dedup key.
1207+ return hashlib .md5 (url .encode (), usedforsecurity = False ).hexdigest ()
11811208
11821209 def is_duplicate (self , url : str ) -> bool :
11831210 h = self ._hash_url (url )
@@ -1263,22 +1290,31 @@ def search_dork(self, dork: str, count: int,
12631290 break
12641291
12651292 # Start producer
1293+ # NOTE: stop_event allows the consumer to signal the producer
1294+ # to abort early (skip/exit/count-reached). Without this, on a
1295+ # retry the previous producer thread would stay alive pushing
1296+ # items into an orphaned queue — wasting RAM/CPU on Android.
12661297 result_queue : queue .Queue = queue .Queue ()
1298+ stop_event = threading .Event ()
12671299
1268- def _producer (dork = dork , batch_size = batch_size ):
1300+ def _producer (dork = dork , batch_size = batch_size ,
1301+ q = result_queue , stop = stop_event ):
12691302 try :
12701303 for item in DDGS ().text (dork , max_results = batch_size ):
1271- result_queue .put (item )
1304+ if stop .is_set ():
1305+ break
1306+ q .put (item )
12721307 except Exception :
12731308 pass
12741309 finally :
1275- result_queue .put (_DONE )
1310+ q .put (_DONE )
12761311
12771312 threading .Thread (target = _producer , daemon = True ).start ()
12781313
12791314 # Consume; poll every 0.25 s so Ctrl+C is always responsive
12801315 while True :
12811316 if _exit_requested or _skip_current :
1317+ stop_event .set () # signal producer to abort
12821318 break
12831319 try :
12841320 r = result_queue .get (timeout = 0.25 )
@@ -1315,6 +1351,7 @@ def _producer(dork=dork, batch_size=batch_size):
13151351 self .stats [f"category_{ entry ['category' ]} " ] += 1
13161352 progress .update (task , completed = min (total_fetched , count ))
13171353 if total_fetched >= count :
1354+ stop_event .set () # producer no longer needed
13181355 break
13191356
13201357 if total_fetched >= count :
@@ -1370,7 +1407,13 @@ def analyze_results(self, results: List[Dict]) -> List[Dict]:
13701407 "status_code" : analysis ["status_code" ],
13711408 })
13721409 progress .advance (task1 )
1373- time .sleep (random .uniform (1 , 2 ) if self .config .get ("stealth_mode" , False ) else 0.5 )
1410+ _interruptible_sleep (random .uniform (1 , 2 ) if self .config .get ("stealth_mode" , False ) else 0.5 )
1411+
1412+ # Reset _skip_current dopo il loop file analysis per evitare che
1413+ # propaghi nel blocco SQLi con un messaggio fuorviante
1414+ if _skip_current and not _exit_requested :
1415+ console .print ("[yellow][~] Ctrl+C — file analysis skipped.[/yellow]" )
1416+ _skip_current = False
13741417
13751418 if self .config .get ("sqli_detection" , False ) and urls_to_test_sqli :
13761419 task2 = progress .add_task ("[cyan]Testing for [red]SQLi[cyan]..." , total = len (urls_to_test_sqli ))
@@ -1392,7 +1435,7 @@ def analyze_results(self, results: List[Dict]) -> List[Dict]:
13921435 )
13931436 progress .advance (task2 )
13941437 if self .config .get ("stealth_mode" , False ):
1395- time . sleep (random .uniform (3 , 6 ))
1438+ _interruptible_sleep (random .uniform (3 , 6 ))
13961439 return results
13971440
13981441 def run_search (self , dorks : List [str ], count : int ):
0 commit comments