Minor refactoring (#5965)

This commit is contained in:
Miroslav Stampar
2025-12-31 14:53:59 +01:00
parent ac2bd503d7
commit e1a509ebb7
5 changed files with 14 additions and 15 deletions

426
lib/utils/gui.py Normal file
View File

@@ -0,0 +1,426 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import os
import re
import socket
import subprocess
import sys
import tempfile
import threading
import webbrowser
from lib.core.common import getSafeExString
from lib.core.common import saveConfig
from lib.core.data import paths
from lib.core.defaults import defaults
from lib.core.enums import MKSTEMP_PREFIX
from lib.core.exception import SqlmapMissingDependence
from lib.core.exception import SqlmapSystemException
from lib.core.settings import DEV_EMAIL_ADDRESS
from lib.core.settings import IS_WIN
from lib.core.settings import ISSUES_PAGE
from lib.core.settings import GIT_PAGE
from lib.core.settings import SITE
from lib.core.settings import VERSION_STRING
from lib.core.settings import WIKI_PAGE
from thirdparty.six.moves import queue as _queue
alive = None
line = ""
process = None
queue = None
def runGui(parser):
try:
from thirdparty.six.moves import tkinter as _tkinter
from thirdparty.six.moves import tkinter_scrolledtext as _tkinter_scrolledtext
from thirdparty.six.moves import tkinter_ttk as _tkinter_ttk
from thirdparty.six.moves import tkinter_messagebox as _tkinter_messagebox
except ImportError as ex:
raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex))
# Reference: https://www.reddit.com/r/learnpython/comments/985umy/limit_user_input_to_only_int_with_tkinter/e4dj9k9?utm_source=share&utm_medium=web2x
class ConstrainedEntry(_tkinter.Entry):
def __init__(self, master=None, **kwargs):
self.var = _tkinter.StringVar()
self.regex = kwargs["regex"]
del kwargs["regex"]
_tkinter.Entry.__init__(self, master, textvariable=self.var, **kwargs)
self.old_value = ''
self.var.trace('w', self.check)
self.get, self.set = self.var.get, self.var.set
def check(self, *args):
if re.search(self.regex, self.get()):
self.old_value = self.get()
else:
self.set(self.old_value)
try:
window = _tkinter.Tk()
except Exception as ex:
errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex)
raise SqlmapSystemException(errMsg)
window.title(VERSION_STRING)
# Set theme and colors
bg_color = "#f5f5f5"
fg_color = "#333333"
accent_color = "#2c7fb8"
window.configure(background=bg_color)
# Configure styles
style = _tkinter_ttk.Style()
# Try to use a more modern theme if available
available_themes = style.theme_names()
if 'clam' in available_themes:
style.theme_use('clam')
elif 'alt' in available_themes:
style.theme_use('alt')
# Configure notebook style
style.configure("TNotebook", background=bg_color)
style.configure("TNotebook.Tab",
padding=[10, 4],
background="#e1e1e1",
font=('Helvetica', 9))
style.map("TNotebook.Tab",
background=[("selected", accent_color), ("active", "#7fcdbb")],
foreground=[("selected", "white"), ("active", "white")])
# Configure button style
style.configure("TButton",
padding=4,
relief="flat",
background=accent_color,
foreground="white",
font=('Helvetica', 9))
style.map("TButton",
background=[('active', '#41b6c4')])
# Reference: https://stackoverflow.com/a/10018670
def center(window):
window.update_idletasks()
width = window.winfo_width()
frm_width = window.winfo_rootx() - window.winfo_x()
win_width = width + 2 * frm_width
height = window.winfo_height()
titlebar_height = window.winfo_rooty() - window.winfo_y()
win_height = height + titlebar_height + frm_width
x = window.winfo_screenwidth() // 2 - win_width // 2
y = window.winfo_screenheight() // 2 - win_height // 2
window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
window.deiconify()
def onKeyPress(event):
global line
global queue
if process:
if event.char == '\b':
line = line[:-1]
else:
line += event.char
def onReturnPress(event):
global line
global queue
if process:
try:
process.stdin.write(("%s\n" % line.strip()).encode())
process.stdin.flush()
except socket.error:
line = ""
event.widget.master.master.destroy()
return "break"
except:
return
event.widget.insert(_tkinter.END, "\n")
return "break"
def run():
global alive
global process
global queue
config = {}
for key in window._widgets:
dest, widget_type = key
widget = window._widgets[key]
if hasattr(widget, "get") and not widget.get():
value = None
elif widget_type == "string":
value = widget.get()
elif widget_type == "float":
value = float(widget.get())
elif widget_type == "int":
value = int(widget.get())
else:
value = bool(widget.var.get())
config[dest] = value
for option in parser.option_list:
# Only set default if not already set by the user
if option.dest not in config or config[option.dest] is None:
config[option.dest] = defaults.get(option.dest, None)
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
os.close(handle)
saveConfig(config, configFile)
def enqueue(stream, queue):
global alive
for line in iter(stream.readline, b''):
queue.put(line)
alive = False
stream.close()
alive = True
process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, bufsize=1, close_fds=not IS_WIN)
# Reference: https://stackoverflow.com/a/4896288
queue = _queue.Queue()
thread = threading.Thread(target=enqueue, args=(process.stdout, queue))
thread.daemon = True
thread.start()
top = _tkinter.Toplevel()
top.title("Console")
top.configure(background=bg_color)
# Create a frame for the console
console_frame = _tkinter.Frame(top, bg=bg_color)
console_frame.pack(fill=_tkinter.BOTH, expand=True, padx=10, pady=10)
# Reference: https://stackoverflow.com/a/13833338
text = _tkinter_scrolledtext.ScrolledText(console_frame, undo=True, wrap=_tkinter.WORD,
bg="#2c3e50", fg="#ecf0f1",
insertbackground="white",
font=('Consolas', 10))
text.bind("<Key>", onKeyPress)
text.bind("<Return>", onReturnPress)
text.pack(fill=_tkinter.BOTH, expand=True)
text.focus()
center(top)
while True:
line = ""
try:
line = queue.get(timeout=.1)
text.insert(_tkinter.END, line)
except _queue.Empty:
text.see(_tkinter.END)
text.update_idletasks()
if not alive:
break
# Create a menu bar
menubar = _tkinter.Menu(window, bg=bg_color, fg=fg_color)
filemenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color)
filemenu.add_command(label="Open", state=_tkinter.DISABLED)
filemenu.add_command(label="Save", state=_tkinter.DISABLED)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=window.quit)
menubar.add_cascade(label="File", menu=filemenu)
menubar.add_command(label="Run", command=run)
helpmenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color)
helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE))
helpmenu.add_command(label="Github pages", command=lambda: webbrowser.open(GIT_PAGE))
helpmenu.add_command(label="Wiki pages", command=lambda: webbrowser.open(WIKI_PAGE))
helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE))
helpmenu.add_separator()
helpmenu.add_command(label="About", command=lambda: _tkinter_messagebox.showinfo("About", "Copyright (c) 2006-2025\n\n (%s)" % DEV_EMAIL_ADDRESS))
menubar.add_cascade(label="Help", menu=helpmenu)
window.config(menu=menubar, bg=bg_color)
window._widgets = {}
# Create header frame
header_frame = _tkinter.Frame(window, bg=bg_color, height=60)
header_frame.pack(fill=_tkinter.X, pady=(0, 5))
header_frame.pack_propagate(0)
# Add header label
title_label = _tkinter.Label(header_frame, text="Configuration",
font=('Helvetica', 14),
fg=accent_color, bg=bg_color)
title_label.pack(side=_tkinter.LEFT, padx=15)
# Add run button in header
run_button = _tkinter_ttk.Button(header_frame, text="Run", command=run, width=12)
run_button.pack(side=_tkinter.RIGHT, padx=15)
# Create notebook
notebook = _tkinter_ttk.Notebook(window)
notebook.pack(expand=1, fill="both", padx=5, pady=(0, 5))
# Store tab information for background loading
tab_frames = {}
tab_canvases = {}
tab_scrollable_frames = {}
tab_groups = {}
# Create empty tabs with scrollable areas first (fast)
for group in parser.option_groups:
# Create a frame with scrollbar for the tab
tab_frame = _tkinter.Frame(notebook, bg=bg_color)
tab_frames[group.title] = tab_frame
# Create a canvas with scrollbar
canvas = _tkinter.Canvas(tab_frame, bg=bg_color, highlightthickness=0)
scrollbar = _tkinter_ttk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview)
scrollable_frame = _tkinter.Frame(canvas, bg=bg_color)
# Store references
tab_canvases[group.title] = canvas
tab_scrollable_frames[group.title] = scrollable_frame
tab_groups[group.title] = group
# Configure the canvas scrolling
scrollable_frame.bind(
"<Configure>",
lambda e, canvas=canvas: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Pack the canvas and scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Add the tab to the notebook
notebook.add(tab_frame, text=group.title)
# Add a loading indicator
loading_label = _tkinter.Label(scrollable_frame, text="Loading options...",
font=('Helvetica', 12),
fg=accent_color, bg=bg_color)
loading_label.pack(expand=True)
# Function to populate a tab in the background
def populate_tab(tab_name):
group = tab_groups[tab_name]
scrollable_frame = tab_scrollable_frames[tab_name]
canvas = tab_canvases[tab_name]
# Remove loading indicator
for child in scrollable_frame.winfo_children():
child.destroy()
# Add content to the scrollable frame
row = 0
if group.get_description():
desc_label = _tkinter.Label(scrollable_frame, text=group.get_description(),
wraplength=600, justify="left",
font=('Helvetica', 9),
fg="#555555", bg=bg_color)
desc_label.grid(row=row, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 5))
row += 1
for option in group.option_list:
# Option label
option_label = _tkinter.Label(scrollable_frame,
text=parser.formatter._format_option_strings(option) + ":",
font=('Helvetica', 9),
fg=fg_color, bg=bg_color,
anchor="w")
option_label.grid(row=row, column=0, sticky="w", padx=10, pady=2)
# Input widget
if option.type == "string":
widget = _tkinter.Entry(scrollable_frame, font=('Helvetica', 9),
relief="sunken", bd=1, width=20)
widget.grid(row=row, column=1, sticky="w", padx=5, pady=2)
elif option.type == "float":
widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\.?\d*\Z",
font=('Helvetica', 9),
relief="sunken", bd=1, width=10)
widget.grid(row=row, column=1, sticky="w", padx=5, pady=2)
elif option.type == "int":
widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\Z",
font=('Helvetica', 9),
relief="sunken", bd=1, width=10)
widget.grid(row=row, column=1, sticky="w", padx=5, pady=2)
else:
var = _tkinter.IntVar()
widget = _tkinter.Checkbutton(scrollable_frame, variable=var,
bg=bg_color, activebackground=bg_color)
widget.var = var
widget.grid(row=row, column=1, sticky="w", padx=5, pady=2)
# Help text (truncated to improve performance)
help_text = option.help
if len(help_text) > 100:
help_text = help_text[:100] + "..."
help_label = _tkinter.Label(scrollable_frame, text=help_text,
font=('Helvetica', 8),
fg="#666666", bg=bg_color,
wraplength=400, justify="left")
help_label.grid(row=row, column=2, sticky="w", padx=5, pady=2)
# Store widget reference
window._widgets[(option.dest, option.type)] = widget
# Set default value
default = defaults.get(option.dest)
if default:
if hasattr(widget, "insert"):
widget.insert(0, default)
elif hasattr(widget, "var"):
widget.var.set(1 if default else 0)
row += 1
# Add some padding at the bottom
_tkinter.Label(scrollable_frame, bg=bg_color, height=1).grid(row=row, column=0)
# Update the scroll region after adding all widgets
canvas.update_idletasks()
canvas.configure(scrollregion=canvas.bbox("all"))
# Update the UI to show the tab is fully loaded
window.update_idletasks()
# Function to populate tabs in the background
def populate_tabs_background():
for tab_name in tab_groups.keys():
# Schedule each tab to be populated with a small delay between them
window.after(100, lambda name=tab_name: populate_tab(name))
# Start populating tabs in the background after a short delay
window.after(500, populate_tabs_background)
# Set minimum window size
window.update()
window.minsize(800, 500)
# Center the window on screen
center(window)
# Start the GUI
window.mainloop()

768
lib/utils/ncgui.py Normal file
View File

@@ -0,0 +1,768 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import os
import subprocess
import sys
import tempfile
try:
import curses
except ImportError:
curses = None
from lib.core.common import getSafeExString
from lib.core.common import saveConfig
from lib.core.data import paths
from lib.core.defaults import defaults
from lib.core.enums import MKSTEMP_PREFIX
from lib.core.exception import SqlmapMissingDependence
from lib.core.exception import SqlmapSystemException
from lib.core.settings import IS_WIN
from thirdparty.six.moves import queue as _queue
from thirdparty.six.moves import configparser as _configparser
class NcursesUI:
def __init__(self, stdscr, parser):
self.stdscr = stdscr
self.parser = parser
self.current_tab = 0
self.current_field = 0
self.scroll_offset = 0
self.tabs = []
self.fields = {}
self.running = False
self.process = None
self.queue = None
# Initialize colors
curses.start_color()
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Active tab
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) # Inactive tab
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Selected field
curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # Help text
curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Error/Important
curses.init_pair(7, curses.COLOR_CYAN, curses.COLOR_BLACK) # Label
# Setup curses
curses.curs_set(1)
self.stdscr.keypad(1)
# Parse option groups
self._parse_options()
def _parse_options(self):
"""Parse command line options into tabs and fields"""
for group in self.parser.option_groups:
tab_data = {
'title': group.title,
'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "",
'options': []
}
for option in group.option_list:
field_data = {
'dest': option.dest,
'label': self._format_option_strings(option),
'help': option.help if option.help else "",
'type': option.type if hasattr(option, 'type') and option.type else 'bool',
'value': '',
'default': defaults.get(option.dest) if defaults.get(option.dest) else None
}
tab_data['options'].append(field_data)
self.fields[(group.title, option.dest)] = field_data
self.tabs.append(tab_data)
def _format_option_strings(self, option):
"""Format option strings for display"""
parts = []
if hasattr(option, '_short_opts') and option._short_opts:
parts.extend(option._short_opts)
if hasattr(option, '_long_opts') and option._long_opts:
parts.extend(option._long_opts)
return ', '.join(parts)
def _draw_header(self):
"""Draw the header bar"""
height, width = self.stdscr.getmaxyx()
header = " sqlmap - Ncurses TUI "
self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
self.stdscr.addstr(0, 0, header.center(width))
self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
def _get_tab_bar_height(self):
"""Calculate how many rows the tab bar uses"""
height, width = self.stdscr.getmaxyx()
y = 1
x = 0
for i, tab in enumerate(self.tabs):
tab_text = " %s " % tab['title']
# Check if tab exceeds width, wrap to next line
if x + len(tab_text) >= width:
y += 1
x = 0
# Stop if we've used too many lines
if y >= 3:
break
x += len(tab_text) + 1
return y
def _draw_tabs(self):
"""Draw the tab bar"""
height, width = self.stdscr.getmaxyx()
y = 1
x = 0
for i, tab in enumerate(self.tabs):
tab_text = " %s " % tab['title']
# Check if tab exceeds width, wrap to next line
if x + len(tab_text) >= width:
y += 1
x = 0
# Stop if we've used too many lines
if y >= 3:
break
if i == self.current_tab:
self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
else:
self.stdscr.attron(curses.color_pair(3))
try:
self.stdscr.addstr(y, x, tab_text)
except:
pass
if i == self.current_tab:
self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
else:
self.stdscr.attroff(curses.color_pair(3))
x += len(tab_text) + 1
def _draw_footer(self):
"""Draw the footer with help text"""
height, width = self.stdscr.getmaxyx()
footer = " [Tab] Next | [Arrows] Navigate | [Enter] Edit | [F2] Run | [F3] Export | [F4] Import | [F10] Quit "
try:
self.stdscr.attron(curses.color_pair(1))
self.stdscr.addstr(height - 1, 0, footer.ljust(width))
self.stdscr.attroff(curses.color_pair(1))
except:
pass
def _draw_current_tab(self):
"""Draw the current tab content"""
height, width = self.stdscr.getmaxyx()
tab = self.tabs[self.current_tab]
# Calculate tab bar height
tab_bar_height = self._get_tab_bar_height()
start_y = tab_bar_height + 1
# Clear content area
for y in range(start_y, height - 1):
try:
self.stdscr.addstr(y, 0, " " * width)
except:
pass
y = start_y
# Draw description if exists
if tab['description']:
desc_lines = self._wrap_text(tab['description'], width - 4)
for line in desc_lines[:2]: # Limit to 2 lines
try:
self.stdscr.attron(curses.color_pair(5))
self.stdscr.addstr(y, 2, line)
self.stdscr.attroff(curses.color_pair(5))
y += 1
except:
pass
y += 1
# Draw options
visible_start = self.scroll_offset
visible_end = visible_start + (height - y - 2)
for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start):
if y >= height - 2:
break
is_selected = (i == self.current_field)
# Draw label
label = option['label'][:25].ljust(25)
try:
if is_selected:
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
else:
self.stdscr.attron(curses.color_pair(7))
self.stdscr.addstr(y, 2, label)
if is_selected:
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
else:
self.stdscr.attroff(curses.color_pair(7))
except:
pass
# Draw value
value_str = ""
if option['type'] == 'bool':
value_str = "[X]" if option['value'] else "[ ]"
else:
value_str = str(option['value']) if option['value'] else ""
if option['default'] and not option['value']:
value_str = "(%s)" % str(option['default'])
value_str = value_str[:30]
try:
if is_selected:
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
self.stdscr.addstr(y, 28, value_str)
if is_selected:
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
except:
pass
# Draw help text
if width > 65:
help_text = option['help'][:width-62] if option['help'] else ""
try:
self.stdscr.attron(curses.color_pair(5))
self.stdscr.addstr(y, 60, help_text)
self.stdscr.attroff(curses.color_pair(5))
except:
pass
y += 1
# Draw scroll indicator
if len(tab['options']) > visible_end - visible_start:
try:
self.stdscr.attron(curses.color_pair(6))
self.stdscr.addstr(height - 2, width - 10, "[More...]")
self.stdscr.attroff(curses.color_pair(6))
except:
pass
def _wrap_text(self, text, width):
"""Wrap text to fit within width"""
words = text.split()
lines = []
current_line = ""
for word in words:
if len(current_line) + len(word) + 1 <= width:
current_line += word + " "
else:
if current_line:
lines.append(current_line.strip())
current_line = word + " "
if current_line:
lines.append(current_line.strip())
return lines
def _edit_field(self):
"""Edit the current field"""
tab = self.tabs[self.current_tab]
if self.current_field >= len(tab['options']):
return
option = tab['options'][self.current_field]
if option['type'] == 'bool':
# Toggle boolean
option['value'] = not option['value']
else:
# Text input
height, width = self.stdscr.getmaxyx()
# Create input window
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
input_win.box()
input_win.attron(curses.color_pair(2))
input_win.addstr(0, 2, " Edit %s " % option['label'][:20])
input_win.attroff(curses.color_pair(2))
input_win.addstr(2, 2, "Value:")
input_win.refresh()
# Get input
curses.echo()
curses.curs_set(1)
# Pre-fill with existing value
current_value = str(option['value']) if option['value'] else ""
input_win.addstr(2, 9, current_value)
input_win.move(2, 9)
try:
new_value = input_win.getstr(2, 9, width - 32).decode('utf-8')
# Validate and convert based on type
if option['type'] == 'int':
try:
option['value'] = int(new_value) if new_value else None
except ValueError:
option['value'] = None
elif option['type'] == 'float':
try:
option['value'] = float(new_value) if new_value else None
except ValueError:
option['value'] = None
else:
option['value'] = new_value if new_value else None
except:
pass
curses.noecho()
curses.curs_set(0)
# Clear input window
input_win.clear()
input_win.refresh()
del input_win
def _export_config(self):
"""Export current configuration to a file"""
height, width = self.stdscr.getmaxyx()
# Create input window
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
input_win.box()
input_win.attron(curses.color_pair(2))
input_win.addstr(0, 2, " Export Configuration ")
input_win.attroff(curses.color_pair(2))
input_win.addstr(2, 2, "File:")
input_win.refresh()
# Get input
curses.echo()
curses.curs_set(1)
try:
filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip()
if filename:
# Collect all field values
config = {}
for tab in self.tabs:
for option in tab['options']:
dest = option['dest']
value = option['value'] if option['value'] else option.get('default')
if option['type'] == 'bool':
config[dest] = bool(value)
elif option['type'] == 'int':
config[dest] = int(value) if value else None
elif option['type'] == 'float':
config[dest] = float(value) if value else None
else:
config[dest] = value
# Set defaults for unset options
for option in self.parser.option_list:
if option.dest not in config or config[option.dest] is None:
config[option.dest] = defaults.get(option.dest, None)
# Save config
try:
saveConfig(config, filename)
# Show success message
input_win.clear()
input_win.box()
input_win.attron(curses.color_pair(5))
input_win.addstr(0, 2, " Export Successful ")
input_win.attroff(curses.color_pair(5))
input_win.addstr(2, 2, "Configuration exported to:")
input_win.addstr(3, 2, filename[:width - 26])
input_win.refresh()
curses.napms(2000)
except Exception as ex:
# Show error message
input_win.clear()
input_win.box()
input_win.attron(curses.color_pair(6))
input_win.addstr(0, 2, " Export Failed ")
input_win.attroff(curses.color_pair(6))
input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26])
input_win.refresh()
curses.napms(2000)
except:
pass
curses.noecho()
curses.curs_set(0)
# Clear input window
input_win.clear()
input_win.refresh()
del input_win
def _import_config(self):
"""Import configuration from a file"""
height, width = self.stdscr.getmaxyx()
# Create input window
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
input_win.box()
input_win.attron(curses.color_pair(2))
input_win.addstr(0, 2, " Import Configuration ")
input_win.attroff(curses.color_pair(2))
input_win.addstr(2, 2, "File:")
input_win.refresh()
# Get input
curses.echo()
curses.curs_set(1)
try:
filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip()
if filename and os.path.isfile(filename):
try:
# Read config file
config = _configparser.ConfigParser()
config.read(filename)
imported_count = 0
# Load values into fields
for tab in self.tabs:
for option in tab['options']:
dest = option['dest']
# Search for option in all sections
for section in config.sections():
if config.has_option(section, dest):
value = config.get(section, dest)
# Convert based on type
if option['type'] == 'bool':
option['value'] = value.lower() in ('true', '1', 'yes', 'on')
elif option['type'] == 'int':
try:
option['value'] = int(value) if value else None
except ValueError:
option['value'] = None
elif option['type'] == 'float':
try:
option['value'] = float(value) if value else None
except ValueError:
option['value'] = None
else:
option['value'] = value if value else None
imported_count += 1
break
# Show success message
input_win.clear()
input_win.box()
input_win.attron(curses.color_pair(5))
input_win.addstr(0, 2, " Import Successful ")
input_win.attroff(curses.color_pair(5))
input_win.addstr(2, 2, "Imported %d options from:" % imported_count)
input_win.addstr(3, 2, filename[:width - 26])
input_win.refresh()
curses.napms(2000)
except Exception as ex:
# Show error message
input_win.clear()
input_win.box()
input_win.attron(curses.color_pair(6))
input_win.addstr(0, 2, " Import Failed ")
input_win.attroff(curses.color_pair(6))
input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26])
input_win.refresh()
curses.napms(2000)
elif filename:
# File not found
input_win.clear()
input_win.box()
input_win.attron(curses.color_pair(6))
input_win.addstr(0, 2, " File Not Found ")
input_win.attroff(curses.color_pair(6))
input_win.addstr(2, 2, "File does not exist:")
input_win.addstr(3, 2, filename[:width - 26])
input_win.refresh()
curses.napms(2000)
except:
pass
curses.noecho()
curses.curs_set(0)
# Clear input window
input_win.clear()
input_win.refresh()
del input_win
def _run_sqlmap(self):
"""Run sqlmap with current configuration"""
config = {}
# Collect all field values
for tab in self.tabs:
for option in tab['options']:
dest = option['dest']
value = option['value'] if option['value'] else option.get('default')
if option['type'] == 'bool':
config[dest] = bool(value)
elif option['type'] == 'int':
config[dest] = int(value) if value else None
elif option['type'] == 'float':
config[dest] = float(value) if value else None
else:
config[dest] = value
# Set defaults for unset options
for option in self.parser.option_list:
if option.dest not in config or config[option.dest] is None:
config[option.dest] = defaults.get(option.dest, None)
# Create temp config file
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
os.close(handle)
saveConfig(config, configFile)
# Show console
self._show_console(configFile)
def _show_console(self, configFile):
"""Show console output from sqlmap"""
height, width = self.stdscr.getmaxyx()
# Create console window
console_win = curses.newwin(height - 4, width - 4, 2, 2)
console_win.box()
console_win.attron(curses.color_pair(2))
console_win.addstr(0, 2, " sqlmap Console - Press Q to close ")
console_win.attroff(curses.color_pair(2))
console_win.refresh()
# Create output area
output_win = console_win.derwin(height - 8, width - 8, 2, 2)
output_win.scrollok(True)
output_win.idlok(True)
# Start sqlmap process
try:
process = subprocess.Popen(
[sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
bufsize=1,
close_fds=not IS_WIN
)
# Make it non-blocking
import fcntl
flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)
output_win.nodelay(True)
console_win.nodelay(True)
lines = []
current_line = ""
while True:
# Check for user input
try:
key = console_win.getch()
if key in (ord('q'), ord('Q')):
# Kill process
process.terminate()
break
elif key == curses.KEY_ENTER or key == 10:
# Send newline to process
if process.poll() is None:
try:
process.stdin.write(b'\n')
process.stdin.flush()
except:
pass
except:
pass
# Read output
try:
chunk = process.stdout.read(1024)
if chunk:
current_line += chunk.decode('utf-8', errors='ignore')
# Split into lines
while '\n' in current_line:
line, current_line = current_line.split('\n', 1)
lines.append(line)
# Keep only last N lines
if len(lines) > 1000:
lines = lines[-1000:]
# Display lines
output_win.clear()
start_line = max(0, len(lines) - (height - 10))
for i, l in enumerate(lines[start_line:]):
try:
output_win.addstr(i, 0, l[:width-10])
except:
pass
output_win.refresh()
console_win.refresh()
except:
pass
# Check if process ended
if process.poll() is not None:
# Read remaining output
try:
remaining = process.stdout.read()
if remaining:
current_line += remaining.decode('utf-8', errors='ignore')
for line in current_line.split('\n'):
if line:
lines.append(line)
except:
pass
# Display final output
output_win.clear()
start_line = max(0, len(lines) - (height - 10))
for i, l in enumerate(lines[start_line:]):
try:
output_win.addstr(i, 0, l[:width-10])
except:
pass
output_win.addstr(height - 9, 0, "--- Process finished. Press Q to close ---")
output_win.refresh()
console_win.refresh()
# Wait for Q
console_win.nodelay(False)
while True:
key = console_win.getch()
if key in (ord('q'), ord('Q')):
break
break
# Small delay
curses.napms(50)
except Exception as ex:
output_win.addstr(0, 0, "Error: %s" % getSafeExString(ex))
output_win.refresh()
console_win.nodelay(False)
console_win.getch()
finally:
# Clean up
try:
os.unlink(configFile)
except:
pass
console_win.nodelay(False)
output_win.nodelay(False)
del output_win
del console_win
def run(self):
"""Main UI loop"""
while True:
self.stdscr.clear()
# Draw UI
self._draw_header()
self._draw_tabs()
self._draw_current_tab()
self._draw_footer()
self.stdscr.refresh()
# Get input
key = self.stdscr.getch()
tab = self.tabs[self.current_tab]
# Handle input
if key == curses.KEY_F10 or key == 27: # F10 or ESC
break
elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow
self.current_tab = (self.current_tab + 1) % len(self.tabs)
self.current_field = 0
self.scroll_offset = 0
elif key == curses.KEY_LEFT: # Left arrow
self.current_tab = (self.current_tab - 1) % len(self.tabs)
self.current_field = 0
self.scroll_offset = 0
elif key == curses.KEY_UP: # Up arrow
if self.current_field > 0:
self.current_field -= 1
# Adjust scroll if needed
if self.current_field < self.scroll_offset:
self.scroll_offset = self.current_field
elif key == curses.KEY_DOWN: # Down arrow
if self.current_field < len(tab['options']) - 1:
self.current_field += 1
# Adjust scroll if needed
height, width = self.stdscr.getmaxyx()
visible_lines = height - 8
if self.current_field >= self.scroll_offset + visible_lines:
self.scroll_offset = self.current_field - visible_lines + 1
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter
self._edit_field()
elif key == curses.KEY_F2: # F2 to run
self._run_sqlmap()
elif key == curses.KEY_F3: # F3 to export
self._export_config()
elif key == curses.KEY_F4: # F4 to import
self._import_config()
elif key == ord(' '): # Space for boolean toggle
option = tab['options'][self.current_field]
if option['type'] == 'bool':
option['value'] = not option['value']
def runTui(parser):
"""Main entry point for ncurses TUI"""
# Check if ncurses is available
if curses is None:
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)")
try:
# Initialize and run
def main(stdscr):
ui = NcursesUI(stdscr, parser)
ui.run()
curses.wrapper(main)
except Exception as ex:
errMsg = "unable to create ncurses UI ('%s')" % getSafeExString(ex)
raise SqlmapSystemException(errMsg)