Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/utils/tui.py
3556 views
1
#!/usr/bin/env python
2
3
"""
4
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5
See the file 'LICENSE' for copying permission
6
"""
7
8
import os
9
import subprocess
10
import sys
11
import tempfile
12
13
try:
14
import curses
15
except ImportError:
16
curses = None
17
18
from lib.core.common import getSafeExString
19
from lib.core.common import saveConfig
20
from lib.core.data import paths
21
from lib.core.defaults import defaults
22
from lib.core.enums import MKSTEMP_PREFIX
23
from lib.core.exception import SqlmapMissingDependence
24
from lib.core.exception import SqlmapSystemException
25
from lib.core.settings import IS_WIN
26
from thirdparty.six.moves import configparser as _configparser
27
28
class NcursesUI:
29
def __init__(self, stdscr, parser):
30
self.stdscr = stdscr
31
self.parser = parser
32
self.current_tab = 0
33
self.current_field = 0
34
self.scroll_offset = 0
35
self.tabs = []
36
self.fields = {}
37
self.running = False
38
self.process = None
39
40
# Initialize colors
41
curses.start_color()
42
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header
43
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Active tab
44
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) # Inactive tab
45
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Selected field
46
curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # Help text
47
curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Error/Important
48
curses.init_pair(7, curses.COLOR_CYAN, curses.COLOR_BLACK) # Label
49
50
# Setup curses
51
curses.curs_set(1)
52
self.stdscr.keypad(1)
53
54
# Parse option groups
55
self._parse_options()
56
57
def _parse_options(self):
58
"""Parse command line options into tabs and fields"""
59
for group in self.parser.option_groups:
60
tab_data = {
61
'title': group.title,
62
'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "",
63
'options': []
64
}
65
66
for option in group.option_list:
67
field_data = {
68
'dest': option.dest,
69
'label': self._format_option_strings(option),
70
'help': option.help if option.help else "",
71
'type': option.type if hasattr(option, 'type') and option.type else 'bool',
72
'value': '',
73
'default': defaults.get(option.dest) if defaults.get(option.dest) else None
74
}
75
tab_data['options'].append(field_data)
76
self.fields[(group.title, option.dest)] = field_data
77
78
self.tabs.append(tab_data)
79
80
def _format_option_strings(self, option):
81
"""Format option strings for display"""
82
parts = []
83
if hasattr(option, '_short_opts') and option._short_opts:
84
parts.extend(option._short_opts)
85
if hasattr(option, '_long_opts') and option._long_opts:
86
parts.extend(option._long_opts)
87
return ', '.join(parts)
88
89
def _draw_header(self):
90
"""Draw the header bar"""
91
height, width = self.stdscr.getmaxyx()
92
header = " sqlmap - ncurses TUI "
93
self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
94
self.stdscr.addstr(0, 0, header.center(width))
95
self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
96
97
def _get_tab_bar_height(self):
98
"""Calculate how many rows the tab bar uses"""
99
height, width = self.stdscr.getmaxyx()
100
y = 1
101
x = 0
102
103
for i, tab in enumerate(self.tabs):
104
tab_text = " %s " % tab['title']
105
106
# Check if tab exceeds width, wrap to next line
107
if x + len(tab_text) >= width:
108
y += 1
109
x = 0
110
# Stop if we've used too many lines
111
if y >= 3:
112
break
113
114
x += len(tab_text) + 1
115
116
return y
117
118
def _draw_tabs(self):
119
"""Draw the tab bar"""
120
height, width = self.stdscr.getmaxyx()
121
y = 1
122
x = 0
123
124
for i, tab in enumerate(self.tabs):
125
tab_text = " %s " % tab['title']
126
127
# Check if tab exceeds width, wrap to next line
128
if x + len(tab_text) >= width:
129
y += 1
130
x = 0
131
# Stop if we've used too many lines
132
if y >= 3:
133
break
134
135
if i == self.current_tab:
136
self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
137
else:
138
self.stdscr.attron(curses.color_pair(3))
139
140
try:
141
self.stdscr.addstr(y, x, tab_text)
142
except:
143
pass
144
145
if i == self.current_tab:
146
self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
147
else:
148
self.stdscr.attroff(curses.color_pair(3))
149
150
x += len(tab_text) + 1
151
152
def _draw_footer(self):
153
"""Draw the footer with help text"""
154
height, width = self.stdscr.getmaxyx()
155
footer = " [Tab] Next | [Arrows] Navigate | [Enter] Edit | [F2] Run | [F3] Export | [F4] Import | [F10] Quit "
156
157
try:
158
self.stdscr.attron(curses.color_pair(1))
159
self.stdscr.addstr(height - 1, 0, footer.ljust(width))
160
self.stdscr.attroff(curses.color_pair(1))
161
except:
162
pass
163
164
def _draw_current_tab(self):
165
"""Draw the current tab content"""
166
height, width = self.stdscr.getmaxyx()
167
tab = self.tabs[self.current_tab]
168
169
# Calculate tab bar height
170
tab_bar_height = self._get_tab_bar_height()
171
start_y = tab_bar_height + 1
172
173
# Clear content area
174
for y in range(start_y, height - 1):
175
try:
176
self.stdscr.addstr(y, 0, " " * width)
177
except:
178
pass
179
180
y = start_y
181
182
# Draw description if exists
183
if tab['description']:
184
desc_lines = self._wrap_text(tab['description'], width - 4)
185
for line in desc_lines[:2]: # Limit to 2 lines
186
try:
187
self.stdscr.attron(curses.color_pair(5))
188
self.stdscr.addstr(y, 2, line)
189
self.stdscr.attroff(curses.color_pair(5))
190
y += 1
191
except:
192
pass
193
y += 1
194
195
# Draw options
196
visible_start = self.scroll_offset
197
visible_end = visible_start + (height - y - 2)
198
199
for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start):
200
if y >= height - 2:
201
break
202
203
is_selected = (i == self.current_field)
204
205
# Draw label
206
label = option['label'][:25].ljust(25)
207
try:
208
if is_selected:
209
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
210
else:
211
self.stdscr.attron(curses.color_pair(7))
212
213
self.stdscr.addstr(y, 2, label)
214
215
if is_selected:
216
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
217
else:
218
self.stdscr.attroff(curses.color_pair(7))
219
except:
220
pass
221
222
# Draw value
223
value_str = ""
224
if option['type'] == 'bool':
225
value = option['value'] if option['value'] is not None else option.get('default')
226
value_str = "[X]" if value else "[ ]"
227
else:
228
value_str = str(option['value']) if option['value'] else ""
229
if option['default'] and not option['value']:
230
value_str = "(%s)" % str(option['default'])
231
232
value_str = value_str[:30]
233
234
try:
235
if is_selected:
236
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
237
self.stdscr.addstr(y, 28, value_str)
238
if is_selected:
239
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
240
except:
241
pass
242
243
# Draw help text
244
if width > 65:
245
help_text = option['help'][:width-62] if option['help'] else ""
246
try:
247
self.stdscr.attron(curses.color_pair(5))
248
self.stdscr.addstr(y, 60, help_text)
249
self.stdscr.attroff(curses.color_pair(5))
250
except:
251
pass
252
253
y += 1
254
255
# Draw scroll indicator
256
if len(tab['options']) > visible_end - visible_start:
257
try:
258
self.stdscr.attron(curses.color_pair(6))
259
self.stdscr.addstr(height - 2, width - 10, "[More...]")
260
self.stdscr.attroff(curses.color_pair(6))
261
except:
262
pass
263
264
def _wrap_text(self, text, width):
265
"""Wrap text to fit within width"""
266
words = text.split()
267
lines = []
268
current_line = ""
269
270
for word in words:
271
if len(current_line) + len(word) + 1 <= width:
272
current_line += word + " "
273
else:
274
if current_line:
275
lines.append(current_line.strip())
276
current_line = word + " "
277
278
if current_line:
279
lines.append(current_line.strip())
280
281
return lines
282
283
def _edit_field(self):
284
"""Edit the current field"""
285
tab = self.tabs[self.current_tab]
286
if self.current_field >= len(tab['options']):
287
return
288
289
option = tab['options'][self.current_field]
290
291
if option['type'] == 'bool':
292
# Toggle boolean
293
option['value'] = not option['value']
294
else:
295
# Text input
296
height, width = self.stdscr.getmaxyx()
297
298
# Create input window
299
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
300
input_win.box()
301
input_win.attron(curses.color_pair(2))
302
input_win.addstr(0, 2, " Edit %s " % option['label'][:20])
303
input_win.attroff(curses.color_pair(2))
304
input_win.addstr(2, 2, "Value:")
305
input_win.refresh()
306
307
# Get input
308
curses.echo()
309
curses.curs_set(1)
310
311
# Pre-fill with existing value
312
current_value = str(option['value']) if option['value'] else ""
313
input_win.addstr(2, 9, current_value)
314
input_win.move(2, 9)
315
316
try:
317
new_value = input_win.getstr(2, 9, width - 32).decode('utf-8')
318
319
# Validate and convert based on type
320
if option['type'] == 'int':
321
try:
322
option['value'] = int(new_value) if new_value else None
323
except ValueError:
324
option['value'] = None
325
elif option['type'] == 'float':
326
try:
327
option['value'] = float(new_value) if new_value else None
328
except ValueError:
329
option['value'] = None
330
else:
331
option['value'] = new_value if new_value else None
332
except:
333
pass
334
335
curses.noecho()
336
curses.curs_set(0)
337
338
# Clear input window
339
input_win.clear()
340
input_win.refresh()
341
del input_win
342
343
def _export_config(self):
344
"""Export current configuration to a file"""
345
height, width = self.stdscr.getmaxyx()
346
347
# Create input window
348
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
349
input_win.box()
350
input_win.attron(curses.color_pair(2))
351
input_win.addstr(0, 2, " Export Configuration ")
352
input_win.attroff(curses.color_pair(2))
353
input_win.addstr(2, 2, "File:")
354
input_win.refresh()
355
356
# Get input
357
curses.echo()
358
curses.curs_set(1)
359
360
try:
361
filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip()
362
363
if filename:
364
# Collect all field values
365
config = {}
366
for tab in self.tabs:
367
for option in tab['options']:
368
dest = option['dest']
369
value = option['value'] if option['value'] is not None else option.get('default')
370
371
if option['type'] == 'bool':
372
config[dest] = bool(value)
373
elif option['type'] == 'int':
374
config[dest] = int(value) if value else None
375
elif option['type'] == 'float':
376
config[dest] = float(value) if value else None
377
else:
378
config[dest] = value
379
380
# Set defaults for unset options
381
for option in self.parser.option_list:
382
if option.dest not in config or config[option.dest] is None:
383
config[option.dest] = defaults.get(option.dest, None)
384
385
# Save config
386
try:
387
saveConfig(config, filename)
388
389
# Show success message
390
input_win.clear()
391
input_win.box()
392
input_win.attron(curses.color_pair(5))
393
input_win.addstr(0, 2, " Export Successful ")
394
input_win.attroff(curses.color_pair(5))
395
input_win.addstr(2, 2, "Configuration exported to:")
396
input_win.addstr(3, 2, filename[:width - 26])
397
input_win.refresh()
398
curses.napms(2000)
399
except Exception as ex:
400
# Show error message
401
input_win.clear()
402
input_win.box()
403
input_win.attron(curses.color_pair(6))
404
input_win.addstr(0, 2, " Export Failed ")
405
input_win.attroff(curses.color_pair(6))
406
input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26])
407
input_win.refresh()
408
curses.napms(2000)
409
except:
410
pass
411
412
curses.noecho()
413
curses.curs_set(0)
414
415
# Clear input window
416
input_win.clear()
417
input_win.refresh()
418
del input_win
419
420
def _import_config(self):
421
"""Import configuration from a file"""
422
height, width = self.stdscr.getmaxyx()
423
424
# Create input window
425
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
426
input_win.box()
427
input_win.attron(curses.color_pair(2))
428
input_win.addstr(0, 2, " Import Configuration ")
429
input_win.attroff(curses.color_pair(2))
430
input_win.addstr(2, 2, "File:")
431
input_win.refresh()
432
433
# Get input
434
curses.echo()
435
curses.curs_set(1)
436
437
try:
438
filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip()
439
440
if filename and os.path.isfile(filename):
441
try:
442
# Read config file
443
config = _configparser.ConfigParser()
444
config.read(filename)
445
446
imported_count = 0
447
448
# Load values into fields
449
for tab in self.tabs:
450
for option in tab['options']:
451
dest = option['dest']
452
453
# Search for option in all sections
454
for section in config.sections():
455
if config.has_option(section, dest):
456
value = config.get(section, dest)
457
458
# Convert based on type
459
if option['type'] == 'bool':
460
option['value'] = value.lower() in ('true', '1', 'yes', 'on')
461
elif option['type'] == 'int':
462
try:
463
option['value'] = int(value) if value else None
464
except ValueError:
465
option['value'] = None
466
elif option['type'] == 'float':
467
try:
468
option['value'] = float(value) if value else None
469
except ValueError:
470
option['value'] = None
471
else:
472
option['value'] = value if value else None
473
474
imported_count += 1
475
break
476
477
# Show success message
478
input_win.clear()
479
input_win.box()
480
input_win.attron(curses.color_pair(5))
481
input_win.addstr(0, 2, " Import Successful ")
482
input_win.attroff(curses.color_pair(5))
483
input_win.addstr(2, 2, "Imported %d options from:" % imported_count)
484
input_win.addstr(3, 2, filename[:width - 26])
485
input_win.refresh()
486
curses.napms(2000)
487
488
except Exception as ex:
489
# Show error message
490
input_win.clear()
491
input_win.box()
492
input_win.attron(curses.color_pair(6))
493
input_win.addstr(0, 2, " Import Failed ")
494
input_win.attroff(curses.color_pair(6))
495
input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26])
496
input_win.refresh()
497
curses.napms(2000)
498
elif filename:
499
# File not found
500
input_win.clear()
501
input_win.box()
502
input_win.attron(curses.color_pair(6))
503
input_win.addstr(0, 2, " File Not Found ")
504
input_win.attroff(curses.color_pair(6))
505
input_win.addstr(2, 2, "File does not exist:")
506
input_win.addstr(3, 2, filename[:width - 26])
507
input_win.refresh()
508
curses.napms(2000)
509
except:
510
pass
511
512
curses.noecho()
513
curses.curs_set(0)
514
515
# Clear input window
516
input_win.clear()
517
input_win.refresh()
518
del input_win
519
520
def _run_sqlmap(self):
521
"""Run sqlmap with current configuration"""
522
config = {}
523
524
# Collect all field values
525
for tab in self.tabs:
526
for option in tab['options']:
527
dest = option['dest']
528
value = option['value'] if option['value'] is not None else option.get('default')
529
530
if option['type'] == 'bool':
531
config[dest] = bool(value)
532
elif option['type'] == 'int':
533
config[dest] = int(value) if value else None
534
elif option['type'] == 'float':
535
config[dest] = float(value) if value else None
536
else:
537
config[dest] = value
538
539
# Set defaults for unset options
540
for option in self.parser.option_list:
541
if option.dest not in config or config[option.dest] is None:
542
config[option.dest] = defaults.get(option.dest, None)
543
544
# Create temp config file
545
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
546
os.close(handle)
547
548
saveConfig(config, configFile)
549
550
# Show console
551
self._show_console(configFile)
552
553
def _show_console(self, configFile):
554
"""Show console output from sqlmap"""
555
height, width = self.stdscr.getmaxyx()
556
557
# Create console window
558
console_win = curses.newwin(height - 4, width - 4, 2, 2)
559
console_win.box()
560
console_win.attron(curses.color_pair(2))
561
console_win.addstr(0, 2, " sqlmap Console - Press Q to close ")
562
console_win.attroff(curses.color_pair(2))
563
console_win.refresh()
564
565
# Create output area
566
output_win = console_win.derwin(height - 8, width - 8, 2, 2)
567
output_win.scrollok(True)
568
output_win.idlok(True)
569
570
# Start sqlmap process
571
try:
572
process = subprocess.Popen(
573
[sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile],
574
shell=False,
575
stdout=subprocess.PIPE,
576
stderr=subprocess.STDOUT,
577
stdin=subprocess.PIPE,
578
bufsize=1,
579
close_fds=not IS_WIN
580
)
581
582
if not IS_WIN:
583
# Make it non-blocking
584
import fcntl
585
flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
586
fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)
587
588
output_win.nodelay(True)
589
console_win.nodelay(True)
590
591
lines = []
592
current_line = ""
593
594
while True:
595
# Check for user input
596
try:
597
key = console_win.getch()
598
if key in (ord('q'), ord('Q')):
599
# Kill process
600
process.terminate()
601
break
602
elif key == curses.KEY_ENTER or key == 10:
603
# Send newline to process
604
if process.poll() is None:
605
try:
606
process.stdin.write(b'\n')
607
process.stdin.flush()
608
except:
609
pass
610
except:
611
pass
612
613
# Read output
614
try:
615
chunk = process.stdout.read(1024)
616
if chunk:
617
current_line += chunk.decode('utf-8', errors='ignore')
618
619
# Split into lines
620
while '\n' in current_line:
621
line, current_line = current_line.split('\n', 1)
622
lines.append(line)
623
624
# Keep only last N lines
625
if len(lines) > 1000:
626
lines = lines[-1000:]
627
628
# Display lines
629
output_win.clear()
630
start_line = max(0, len(lines) - (height - 10))
631
for i, l in enumerate(lines[start_line:]):
632
try:
633
output_win.addstr(i, 0, l[:width-10])
634
except:
635
pass
636
output_win.refresh()
637
console_win.refresh()
638
except:
639
pass
640
641
# Check if process ended
642
if process.poll() is not None:
643
# Read remaining output
644
try:
645
remaining = process.stdout.read()
646
if remaining:
647
current_line += remaining.decode('utf-8', errors='ignore')
648
for line in current_line.split('\n'):
649
if line:
650
lines.append(line)
651
except:
652
pass
653
654
# Display final output
655
output_win.clear()
656
start_line = max(0, len(lines) - (height - 10))
657
for i, l in enumerate(lines[start_line:]):
658
try:
659
output_win.addstr(i, 0, l[:width-10])
660
except:
661
pass
662
663
output_win.addstr(height - 9, 0, "--- Process finished. Press Q to close ---")
664
output_win.refresh()
665
console_win.refresh()
666
667
# Wait for Q
668
console_win.nodelay(False)
669
while True:
670
key = console_win.getch()
671
if key in (ord('q'), ord('Q')):
672
break
673
674
break
675
676
# Small delay
677
curses.napms(50)
678
679
except Exception as ex:
680
output_win.addstr(0, 0, "Error: %s" % getSafeExString(ex))
681
output_win.refresh()
682
console_win.nodelay(False)
683
console_win.getch()
684
685
finally:
686
# Clean up
687
try:
688
os.unlink(configFile)
689
except:
690
pass
691
692
console_win.nodelay(False)
693
output_win.nodelay(False)
694
del output_win
695
del console_win
696
697
def run(self):
698
"""Main UI loop"""
699
while True:
700
self.stdscr.clear()
701
702
# Draw UI
703
self._draw_header()
704
self._draw_tabs()
705
self._draw_current_tab()
706
self._draw_footer()
707
708
self.stdscr.refresh()
709
710
# Get input
711
key = self.stdscr.getch()
712
713
tab = self.tabs[self.current_tab]
714
715
# Handle input
716
if key == curses.KEY_F10 or key == 27: # F10 or ESC
717
break
718
elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow
719
self.current_tab = (self.current_tab + 1) % len(self.tabs)
720
self.current_field = 0
721
self.scroll_offset = 0
722
elif key == curses.KEY_LEFT: # Left arrow
723
self.current_tab = (self.current_tab - 1) % len(self.tabs)
724
self.current_field = 0
725
self.scroll_offset = 0
726
elif key == curses.KEY_UP: # Up arrow
727
if self.current_field > 0:
728
self.current_field -= 1
729
# Adjust scroll if needed
730
if self.current_field < self.scroll_offset:
731
self.scroll_offset = self.current_field
732
elif key == curses.KEY_DOWN: # Down arrow
733
if self.current_field < len(tab['options']) - 1:
734
self.current_field += 1
735
# Adjust scroll if needed
736
height, width = self.stdscr.getmaxyx()
737
visible_lines = height - 8
738
if self.current_field >= self.scroll_offset + visible_lines:
739
self.scroll_offset = self.current_field - visible_lines + 1
740
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter
741
self._edit_field()
742
elif key == curses.KEY_F2: # F2 to run
743
self._run_sqlmap()
744
elif key == curses.KEY_F3: # F3 to export
745
self._export_config()
746
elif key == curses.KEY_F4: # F4 to import
747
self._import_config()
748
elif key == ord(' '): # Space for boolean toggle
749
option = tab['options'][self.current_field]
750
if option['type'] == 'bool':
751
option['value'] = not option['value']
752
753
def runTui(parser):
754
"""Main entry point for ncurses TUI"""
755
# Check if ncurses is available
756
if curses is None:
757
raise SqlmapMissingDependence("missing 'curses' module (optional Python module). Use a Python build that includes curses/ncurses, or install the platform-provided equivalent (e.g. for Windows: pip install windows-curses)")
758
try:
759
# Initialize and run
760
def main(stdscr):
761
ui = NcursesUI(stdscr, parser)
762
ui.run()
763
764
curses.wrapper(main)
765
766
except Exception as ex:
767
errMsg = "unable to create ncurses UI ('%s')" % getSafeExString(ex)
768
raise SqlmapSystemException(errMsg)
769
770