mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-01-01 20:39:03 +00:00
Minor refactoring (#5965)
This commit is contained in:
426
lib/utils/gui.py
Normal file
426
lib/utils/gui.py
Normal 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
768
lib/utils/ncgui.py
Normal 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)
|
||||
Reference in New Issue
Block a user