From 1db09d5a9806edf967215f2267c5e5b5c04358e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 19 Jul 2026 16:31:08 +0200 Subject: [PATCH] Improved --gui --- lib/core/settings.py | 2 +- lib/utils/gui.py | 1529 ++++++++++++++++++++++++++++++++---------- 2 files changed, 1194 insertions(+), 337 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index acf6fdf3f..a48c74a56 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.134" +VERSION = "1.10.7.135" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/utils/gui.py b/lib/utils/gui.py index 97beb328b..452160bb4 100644 --- a/lib/utils/gui.py +++ b/lib/utils/gui.py @@ -5,11 +5,13 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ +import io import os import subprocess import sys import tempfile import threading +import time import webbrowser from lib.core.common import getSafeExString @@ -28,35 +30,88 @@ from lib.core.settings import VERSION_STRING from lib.core.settings import WIKI_PAGE from thirdparty.six.moves import queue as _queue -# Classic Windows (NT/9x) palette: silver 3D face, navy title/selection, white sunken fields, -# black text, and saturated VGA-style accents for the icons (presentation only) +try: + _text_type = unicode +except NameError: + _text_type = str + +_binary_type = str if sys.version_info[0] < 3 else bytes +_clock = getattr(time, "perf_counter", getattr(time, "clock", time.time)) + +def _toText(value): + """Return a Unicode text value on both Python 2.7 and Python 3.x.""" + if value is None: + return u"" + if isinstance(value, _text_type): + return value + if isinstance(value, _binary_type): + try: + return value.decode("utf-8", "replace") + except Exception: + return _text_type(value) + try: + return _text_type(value) + except Exception: + return _text_type(repr(value)) + +def _toBytes(value): + """Return UTF-8 bytes suitable for a binary subprocess pipe.""" + if isinstance(value, _binary_type): + return value + return _toText(value).encode("utf-8", "replace") + +def _waitForProcess(process, timeout): + """Python 2 compatible replacement for Popen.wait(timeout=...).""" + deadline = _clock() + max(0.0, timeout) + while process.poll() is None and _clock() < deadline: + time.sleep(0.03) + return process.poll() + +def _list2cmdline(arguments): + values = [_toText(_) for _ in arguments] + if sys.version_info[0] < 3: + return _toText(subprocess.list2cmdline([_toBytes(_) for _ in values])) + return _toText(subprocess.list2cmdline(values)) + +# A restrained security-tool palette: the layout stays familiar, while the darker +# navigation, cyan accents and terminal surfaces add a light Havij-era character. PALETTE = { - "base": "#c0c0c0", # window / control face (silver) - "mantle": "#c0c0c0", # bars (classic is uniform gray, separated by bevels) - "crust": "#ffffff", # console / edit background - "surface0": "#ffffff", # field (edit) background - "surface1": "#808080", # 3D shadow - "surface2": "#dfdfdf", # 3D light (soft) - "light": "#ffffff", # 3D highlight - "dark": "#404040", # 3D dark shadow - "text": "#000000", - "subtext": "#000000", - "overlay": "#404040", - "title2": "#1084d0", # active title-bar gradient end - "blue": "#000080", # navy: title, selection, accents - "sapphire": "#0050b0", - "sky": "#0070c0", - "green": "#008000", - "teal": "#008080", - "red": "#c00000", - "maroon": "#800000", - "mauve": "#9000a8", - "pink": "#c000b0", - "peach": "#c06000", - "yellow": "#c08000", - "lavender": "#4858c0", - "flamingo": "#c04070", - "gold": "#e0a800", + "base": "#d7dce1", + "mantle": "#243545", + "crust": "#101820", + "surface0": "#f8fafb", + "surface1": "#8d98a3", + "surface2": "#e7ebef", + "light": "#ffffff", + "dark": "#3c4650", + "text": "#17212b", + "subtext": "#33414f", + "overlay": "#657381", + "title2": "#0b79a5", + "blue": "#164d73", + "sapphire": "#087caf", + "sky": "#169ec1", + "green": "#2d9659", + "teal": "#178b86", + "red": "#bd3f45", + "maroon": "#8c3d56", + "mauve": "#86549a", + "pink": "#b34e83", + "peach": "#c56d35", + "yellow": "#b78a18", + "lavender": "#6172b8", + "flamingo": "#bf5b72", + "gold": "#d29b22", + "navText": "#eef4f8", + "navMuted": "#a9bac8", + "navHover": "#31485d", + "panel": "#eef2f5", + "border": "#9ca7b1", + "success": "#2f9b5b", + "command": "#13232e", + "commandText": "#8de19b", + "consoleText": "#d8e7de", + "consoleMuted": "#8fa69a", } # a distinct accent color per section, so the sidebar icons read as a colorful, scannable set @@ -98,6 +153,8 @@ NAV_ALIASES = { TARGET_PLACEHOLDER = "http://www.target.com/vuln.php?id=1" HINT_DEFAULT = "Hover or focus a field to see what it does." +MAX_CONSOLE_LINES = 12000 +MAX_SEARCH_RESULTS = 12 # --- parser-backend compatibility (works for both optparse and argparse objects) --- @@ -154,34 +211,143 @@ def _optValueType(option): def _optionLabel(option): return ", ".join(_optStrings(option)) or (_optDest(option) or "") -class _Tooltip(object): - """Lightweight hover tooltip for a widget""" +def _preferredFlag(option): + strings = _optStrings(option) + longOptions = [_ for _ in strings if _.startswith("--")] + return (longOptions or strings or [""])[0] - def __init__(self, widget, text, tk, palette): - self._widget = widget - self._text = text +def _quoteArg(value): + value = _toText(value) + if IS_WIN: + return _list2cmdline([value]) + if not value: + return u"''" + safe = u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@%+=:,./-" + if all(character in safe for character in value): + return value + return u"'" + value.replace(u"'", u"'\"'\"'") + u"'" + +class _TooltipManager(object): + """One shared tooltip/hint dispatcher for every option control. + + Per-widget Python/Tcl bindings are surprisingly expensive when a pane contains + dozens of options. Widgets only receive a small Python attribute; four global + bindings handle the whole application. + """ + + def __init__(self, owner, root, tk, palette, delay=500): + self._owner = owner + self._root = root self._tk = tk self._palette = palette + self._delay = delay + self._widget = None self._tip = None - widget.bind("", self._show, add="+") - widget.bind("", self._hide, add="+") - widget.bind("", self._hide, add="+") + self._job = None + root.bind_all("", self._enter, add="+") + root.bind_all("", self._leave, add="+") + root.bind_all("", self._focusIn, add="+") + root.bind_all("", self._focusOut, add="+") + root.bind_all("", self._hide, add="+") - def _show(self, event=None): - if self._tip or not self._text: + def attach(self, widget, text): + if text: + widget._sqlmap_help = text + + def _textFor(self, widget): + return getattr(widget, "_sqlmap_help", "") + + def _setHint(self, text): + try: + if hasattr(self._owner, "hint"): + self._owner.hint.set(text or HINT_DEFAULT) + except Exception: + pass + + def _enter(self, event): + text = self._textFor(event.widget) + if not text: return - x = self._widget.winfo_rootx() + 18 - y = self._widget.winfo_rooty() + self._widget.winfo_height() + 6 - self._tip = tw = self._tk.Toplevel(self._widget) - tw.wm_overrideredirect(True) - tw.wm_geometry("+%d+%d" % (x, y)) - self._tk.Label(tw, text=self._text, justify="left", background=self._palette["surface0"], - foreground=self._palette["text"], relief="flat", borderwidth=0, - wraplength=460, padx=10, pady=7).pack() + self._widget = event.widget + self._setHint(text) + self._cancel() + try: + self._job = self._root.after(self._delay, self._show) + except Exception: + self._job = None + + def _leave(self, event): + if event.widget is self._widget: + self._widget = None + self._cancel() + self._hide() + self._setHint(HINT_DEFAULT) + + def _focusIn(self, event): + text = self._textFor(event.widget) + if text: + self._setHint(text) + + def _focusOut(self, event): + if self._textFor(event.widget): + self._setHint(HINT_DEFAULT) + + def _cancel(self): + if self._job is not None: + try: + self._root.after_cancel(self._job) + except Exception: + pass + self._job = None + + def _show(self): + self._job = None + widget = self._widget + text = self._textFor(widget) if widget is not None else "" + if not text: + return + try: + if not widget.winfo_exists(): + return + x = widget.winfo_rootx() + 18 + y = widget.winfo_rooty() + widget.winfo_height() + 6 + self._tip = tw = self._tk.Toplevel(widget) + # Toplevels are initially mapped by Tk at the default 0,0 position. + # Keep the tooltip withdrawn until its children have been measured and + # its final geometry has been assigned; otherwise X11 briefly paints an + # empty box in the screen corner before the real tooltip appears. + tw.withdraw() + tw.wm_overrideredirect(True) + try: + tw.wm_transient(self._root) + except Exception: + pass + self._tk.Label(tw, text=text, justify="left", background=self._palette["surface0"], + foreground=self._palette["text"], relief="solid", borderwidth=1, + wraplength=460, padx=10, pady=7).pack() + tw.update_idletasks() + width = max(1, tw.winfo_reqwidth()) + height = max(1, tw.winfo_reqheight()) + x = min(x, max(0, tw.winfo_screenwidth() - width - 8)) + y = min(y, max(0, tw.winfo_screenheight() - height - 8)) + tw.wm_geometry("%dx%d+%d+%d" % (width, height, x, y)) + tw.deiconify() + tw.lift() + except Exception: + if self._tip is not None: + try: + self._tip.destroy() + except Exception: + pass + self._tip = None def _hide(self, event=None): - if self._tip: - self._tip.destroy() + self._cancel() + if self._tip is not None: + try: + self._tip.destroy() + except Exception: + pass self._tip = None class SqlmapGui(object): @@ -194,36 +360,128 @@ class SqlmapGui(object): self.filedialog = filedialog self.font = font - self.widgets = {} # dest -> (type, shared Tk variable) - self.vars = {} # dest -> shared Tk variable (one per option, bound to every widget for it) + self.widgets = {} # dest -> (type, shared effective-value Tk variable) + self.vars = {} # dest -> shared Tk variable (one per option) self.optionByDest = {} + self.optionOrder = [] + self.sectionByDest = {} + self.searchIndex = [] for group in _parserGroups(parser): + title = _groupTitle(group) for option in _groupOptions(group): - if _optDest(option): - self.optionByDest[_optDest(option)] = option + dest = _optDest(option) + if dest: + if dest not in self.optionByDest: + self.optionOrder.append(dest) + self.optionByDest[dest] = option + self.sectionByDest[dest] = title + + for index, dest in enumerate(self.optionOrder): + option = self.optionByDest[dest] + section = self.sectionByDest.get(dest, "") + label = _optionLabel(option) + flag = _preferredFlag(option) + self.searchIndex.append(( + dest, + index, + label, + section, + flag, + " ".join((label, dest, section, _optHelp(option))).lower(), + )) self.panes = {} # name -> outer frame - self.navItems = {} # name -> (row frame, accent strip, icon canvas, label) + self.navItems = {} # name -> (row frame, accent strip, icon canvas, label, badge) self.canvases = {} # name -> canvas (for wheel binding) self.inners = {} # name -> scrollable inner frame (populated lazily) self.builders = {} # name -> callable that populates the inner frame self.built = set() # names whose content has been built + self.buildStates = {} # name -> generator for incremental pane construction + self._prebuildQueue = [] + self._prebuildJob = None self.badges = {} # name -> sidebar count badge label self.sectionDests = {} # name -> [option dests in that section] self.paneOrder = [] # nav order, for Up/Down navigation self.currentPane = None + self.controlsByDest = {} # dest -> [(pane name, interactive widget)] + self.searchMatches = [] + self.process = None - self.alive = False - self.queue = None + self.processQueue = None + self.processConfigFile = None + self.consoleWindow = None + self.consoleText = None + self.consoleStatus = None + self._runSerial = 0 + self._refreshJob = None + self._searchJob = None + self._headerJob = None + self._suspendRefresh = False try: self.window = tk.Tk() except Exception as ex: raise SqlmapSystemException("unable to create GUI window ('%s')" % getSafeExString(ex)) + self.tooltip = _TooltipManager(self, self.window, tk, PALETTE) + self._initializeVariables() self._initFonts() self._initStyle() self._buildLayout() + self.window.protocol("WM_DELETE_WINDOW", self._closeApplication) + + def _initializeVariables(self): + for dest in self.optionOrder: + option = self.optionByDest[dest] + isBool = not _optTakesValue(option) + otype = "bool" if isBool else _optValueType(option) + default = defaults.get(dest) + if isBool: + var = self.tk.BooleanVar(value=bool(default)) + else: + var = self.tk.StringVar(value="" if default in (None, False) else default) + self.vars[dest] = var + self.widgets[dest] = (otype, var) + try: + var.trace("w", self._onOptionChanged) + except Exception: + pass + + def _onOptionChanged(self, *unused): + if not self._suspendRefresh: + self._scheduleRefresh() + + def _scheduleRefresh(self, delay=70): + if self._refreshJob is not None: + try: + self.window.after_cancel(self._refreshJob) + except Exception: + pass + self._refreshJob = self.window.after(delay, self._refreshDerivedState) + + def _refreshDerivedState(self): + self._refreshJob = None + self._updateStats() + self.command.set(self._buildCommandString()) + self._updateStatusLight() + + def _updateStatusLight(self): + try: + canvas = self.statusLight + except AttributeError: + return + try: + canvas.delete("all") + if self._isRunning(): + color = PALETTE["success"] + elif any(self._isOptionSet(_) for _ in self.widgets): + color = PALETTE["sky"] + else: + color = PALETTE["surface1"] + canvas.create_oval(2, 2, 10, 10, fill=color, outline=PALETTE["dark"]) + except Exception: + pass + def _initFonts(self): family = self.font.nametofont("TkDefaultFont").actual("family") @@ -239,82 +497,142 @@ class SqlmapGui(object): def _initStyle(self): p = PALETTE - face, light, light2, shadow, dark = p["base"], p["light"], p["surface2"], p["surface1"], p["dark"] - navy, white, black, field = p["blue"], "#ffffff", p["text"], p["surface0"] + face = p["base"] + field = p["surface0"] style = self.ttk.Style() if "clam" in style.theme_names(): style.theme_use("clam") - style.configure(".", background=face, foreground=black, fieldbackground=field, - bordercolor=shadow, lightcolor=light, darkcolor=shadow, - troughcolor=face, focuscolor=face, insertcolor=black, font=self.fonts["body"]) + style.configure(".", background=face, foreground=p["text"], fieldbackground=field, + bordercolor=p["border"], lightcolor=p["light"], darkcolor=p["surface1"], + troughcolor=p["surface2"], focuscolor=p["blue"], insertcolor=p["text"], + font=self.fonts["body"]) - for name in ("TFrame", "Bar.TFrame", "Nav.TFrame", "Card.TFrame"): - style.configure(name, background=face) + style.configure("TFrame", background=face) + style.configure("Bar.TFrame", background=p["panel"]) + style.configure("Nav.TFrame", background=p["mantle"]) + style.configure("Card.TFrame", background=p["panel"]) + style.configure("Panel.TFrame", background=p["surface0"]) + style.configure("PaneHeader.TFrame", background=p["surface0"]) - style.configure("TLabel", background=face, foreground=black) - style.configure("Title.TLabel", background=navy, foreground=white, font=self.fonts["title"]) - style.configure("Subtitle.TLabel", background=navy, foreground=white, font=self.fonts["subtitle"]) - style.configure("Hint.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) - style.configure("Field.TLabel", background=face, foreground=black) - style.configure("Desc.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) - style.configure("Pane.TLabel", background=face, foreground=navy, font=self.fonts["title"]) - style.configure("Stat.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) - style.configure("Prompt.TLabel", background=field, foreground=black, font=self.fonts["mono"]) + style.configure("TLabel", background=face, foreground=p["text"]) + style.configure("Title.TLabel", background=p["blue"], foreground="#ffffff", font=self.fonts["title"]) + style.configure("Subtitle.TLabel", background=p["blue"], foreground="#dceaf2", font=self.fonts["subtitle"]) + style.configure("Hint.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"]) + style.configure("PanelHint.TLabel", background=p["surface0"], foreground=p["overlay"], font=self.fonts["small"]) + style.configure("PanelLabel.TLabel", background=p["surface0"], foreground=p["blue"], font=self.fonts["bodyBold"]) + style.configure("NavHint.TLabel", background=p["mantle"], foreground=p["navMuted"], font=self.fonts["small"]) + style.configure("NavTitle.TLabel", background=p["mantle"], foreground=p["navText"], font=self.fonts["bodyBold"]) + style.configure("Field.TLabel", background=p["panel"], foreground=p["text"]) + style.configure("Desc.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Pane.TLabel", background=p["surface0"], foreground=p["blue"], font=self.fonts["title"]) + style.configure("PaneCount.TLabel", background=p["surface0"], foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Stat.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Prompt.TLabel", background=field, foreground=p["text"], font=self.fonts["mono"]) - # classic raised 3D push button - style.configure("TButton", background=face, foreground=black, relief="raised", borderwidth=2, - lightcolor=light, darkcolor=dark, bordercolor=shadow, focuscolor=black, padding=(12, 4)) - style.map("TButton", background=[("active", face)], relief=[("pressed", "sunken")]) + style.configure("TButton", background=p["surface2"], foreground=p["text"], relief="raised", borderwidth=1, + lightcolor=p["light"], darkcolor=p["surface1"], bordercolor=p["border"], + focuscolor=p["blue"], padding=(11, 5)) + style.map("TButton", background=[("active", p["surface0"]), ("pressed", p["surface1"])], + relief=[("pressed", "sunken")]) + style.configure("Tool.TButton", padding=(9, 4), font=self.fonts["small"]) + style.configure("Primary.TButton", background=p["success"], foreground="#ffffff", bordercolor=p["green"], + lightcolor="#78c89a", darkcolor="#17643a", padding=(12, 5), font=self.fonts["bodyBold"]) + style.map("Primary.TButton", background=[("active", "#39aa68"), ("pressed", "#247c49")], + foreground=[("disabled", "#d7e4dc")]) - # sunken white edit fields - for name in ("TEntry", "Target.TEntry"): - style.configure(name, fieldbackground=field, foreground=black, relief="sunken", borderwidth=2, - bordercolor=shadow, lightcolor=shadow, darkcolor=light, insertcolor=black, padding=4) + style.configure("TEntry", fieldbackground=field, foreground=p["text"], relief="sunken", borderwidth=1, + bordercolor=p["border"], lightcolor=p["surface1"], darkcolor=p["light"], + insertcolor=p["text"], padding=5) + style.configure("Target.TEntry", fieldbackground="#ffffff", foreground=p["text"], relief="sunken", borderwidth=1, + bordercolor=p["sapphire"], lightcolor=p["surface1"], darkcolor=p["light"], + insertcolor=p["text"], padding=7, font=self.fonts["body"]) + style.configure("Search.TEntry", fieldbackground="#192936", foreground=p["navText"], relief="flat", borderwidth=1, + bordercolor="#4c6376", lightcolor="#4c6376", darkcolor="#17242f", + insertcolor="#ffffff", padding=6) - style.configure("TCheckbutton", background=face, foreground=black, focuscolor=face, padding=2, - indicatorbackground=field, indicatorforeground=black, indicatorrelief="sunken", indicatorborderwidth=2, - bordercolor=shadow, lightcolor=shadow, darkcolor=light) - style.map("TCheckbutton", background=[("active", face)], indicatorbackground=[("active", field), ("selected", field)]) + style.configure("TCheckbutton", background=p["panel"], foreground=p["text"], focuscolor=p["panel"], padding=2, + indicatorbackground=field, indicatorforeground=p["blue"], indicatorrelief="sunken", + indicatorborderwidth=1, bordercolor=p["border"], lightcolor=p["surface1"], darkcolor=p["light"]) + style.map("TCheckbutton", background=[("active", p["panel"])], + indicatorbackground=[("active", field), ("selected", field)]) - style.configure("TCombobox", fieldbackground=field, background=face, foreground=black, arrowcolor=black, - relief="sunken", borderwidth=2, bordercolor=shadow, lightcolor=shadow, darkcolor=light, padding=3) + style.configure("TCombobox", fieldbackground=field, background=p["surface2"], foreground=p["text"], + arrowcolor=p["blue"], relief="sunken", borderwidth=1, bordercolor=p["border"], + lightcolor=p["surface1"], darkcolor=p["light"], padding=4) - # classic chunky scrollbar (raised gray thumb, light trough) - style.configure("Vertical.TScrollbar", background=face, troughcolor=light2, bordercolor=shadow, - lightcolor=light, darkcolor=dark, arrowcolor=black, relief="raised", width=17) - style.map("Vertical.TScrollbar", background=[("active", face)]) + style.configure("Vertical.TScrollbar", background=p["surface2"], troughcolor=p["panel"], + bordercolor=p["border"], lightcolor=p["light"], darkcolor=p["surface1"], + arrowcolor=p["text"], relief="raised", width=16) + style.map("Vertical.TScrollbar", background=[("active", p["surface0"])]) self.window.configure(background=face) - # --- layout --------------------------------------------------------- - def _buildLayout(self): tk = self.tk - self.window.title("sqlmap") - self.window.minsize(960, 680) + p = PALETTE + self.window.title("sqlmap GUI") + self.window.minsize(980, 690) self._buildMenu() self._buildHeader() - target = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 12, 20, 14)) - target.pack(fill=tk.X) - labelRow = self.ttk.Frame(target, style="Bar.TFrame") - labelRow.pack(fill=tk.X, pady=(0, 4)) - self.ttk.Label(labelRow, text="TARGET URL", style="Hint.TLabel").pack(side=tk.LEFT) - self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="Stat.TLabel").pack(side=tk.LEFT) + targetShell = tk.Frame(self.window, background=p["border"], borderwidth=0) + targetShell.pack(fill=tk.X, padx=16, pady=(10, 8)) + target = self.ttk.Frame(targetShell, style="Panel.TFrame", padding=(14, 10, 14, 12)) + target.pack(fill=tk.X, padx=1, pady=1) + tk.Frame(target, background=p["red"], height=3).pack(fill=tk.X, pady=(0, 9)) + + labelRow = self.ttk.Frame(target, style="Panel.TFrame") + labelRow.pack(fill=tk.X, pady=(0, 5)) + self.ttk.Label(labelRow, text="TARGET URL", style="PanelLabel.TLabel").pack(side=tk.LEFT) + self.ttk.Label(labelRow, text="Ctrl+L", style="PanelHint.TLabel").pack(side=tk.RIGHT) + self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="PanelHint.TLabel").pack(side=tk.LEFT) + + targetRow = self.ttk.Frame(target, style="Panel.TFrame") + targetRow.pack(fill=tk.X) urlVar = self._destVar("url", False) - self.targetEntry = self.ttk.Entry(target, style="Target.TEntry", textvariable=urlVar) - self.targetEntry.pack(fill=tk.X, ipady=2) - self.widgets["url"] = ("string", urlVar) + self.targetEntry = self.ttk.Entry(targetRow, style="Target.TEntry", textvariable=urlVar) + self.targetEntry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=1) + self.ttk.Button(targetRow, text="Paste", style="Tool.TButton", command=self._pasteTarget, + takefocus=False).pack(side=tk.LEFT, padx=(8, 0)) + self.ttk.Button(targetRow, text="Clear", style="Tool.TButton", command=self._clearTarget, + takefocus=False).pack(side=tk.LEFT, padx=(6, 0)) + self.controlsByDest.setdefault("url", []).append((None, self.targetEntry)) body = self.ttk.Frame(self.window, style="TFrame") body.pack(expand=True, fill=tk.BOTH) - navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=202) + navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=224) navHolder.pack(side=tk.LEFT, fill=tk.Y) navHolder.pack_propagate(False) - self.navCanvas = tk.Canvas(navHolder, background=PALETTE["mantle"], highlightthickness=0, borderwidth=0) - navScroll = self.ttk.Scrollbar(navHolder, orient="vertical", command=self.navCanvas.yview, style="Vertical.TScrollbar") + + searchBar = self.ttk.Frame(navHolder, style="Nav.TFrame", padding=(11, 11, 11, 8)) + searchBar.pack(fill=tk.X) + searchTitle = self.ttk.Frame(searchBar, style="Nav.TFrame") + searchTitle.pack(fill=tk.X, pady=(0, 5)) + self.ttk.Label(searchTitle, text="OPTION FINDER", style="NavTitle.TLabel").pack(side=tk.LEFT) + self.ttk.Label(searchTitle, text="Ctrl+K", style="NavHint.TLabel").pack(side=tk.RIGHT) + self.searchVar = tk.StringVar(value="") + self.searchEntry = self.ttk.Entry(searchBar, style="Search.TEntry", textvariable=self.searchVar) + self.searchEntry.pack(fill=tk.X) + self.searchEntry.bind("", self._activateSearchResult) + self.searchEntry.bind("", self._searchMoveDown) + try: + self.searchVar.trace("w", self._scheduleSearch) + except Exception: + pass + + self.searchList = tk.Listbox(navHolder, height=6, activestyle="dotbox", exportselection=False, + bg="#192936", fg=p["navText"], selectbackground=p["sapphire"], + selectforeground="#ffffff", relief="flat", borderwidth=1, + highlightthickness=1, highlightbackground="#4c6376", + font=self.fonts["small"]) + self.searchList.bind("", self._clickSearchResult) + self.searchList.bind("", self._activateSearchResult) + + self.navCanvas = tk.Canvas(navHolder, background=p["mantle"], highlightthickness=0, borderwidth=0) + navScroll = self.ttk.Scrollbar(navHolder, orient="vertical", command=self.navCanvas.yview, + style="Vertical.TScrollbar") self.nav = self.ttk.Frame(self.navCanvas, style="Nav.TFrame") self.nav.bind("", lambda e: self.navCanvas.configure(scrollregion=self.navCanvas.bbox("all"))) navWin = self.navCanvas.create_window((0, 0), window=self.nav, anchor="nw") @@ -323,24 +641,31 @@ class SqlmapGui(object): self.navCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) navScroll.pack(side=tk.RIGHT, fill=tk.Y) - tk.Frame(body, background=PALETTE["surface1"], width=1).pack(side=tk.LEFT, fill=tk.Y) + tk.Frame(body, background=p["border"], width=1).pack(side=tk.LEFT, fill=tk.Y) self.content = self.ttk.Frame(body, style="Card.TFrame") self.content.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) - cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 8)) + cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(16, 8)) cmdBar.pack(fill=tk.X) - self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) - self.ttk.Button(cmdBar, text="Copy", command=self._copyCommand, takefocus=False).pack(side=tk.RIGHT, padx=(8, 0)) + self.ttk.Label(cmdBar, text=">_", style="PanelLabel.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.ttk.Button(cmdBar, text="Copy", style="Tool.TButton", command=self._copyCommand, + takefocus=False).pack(side=tk.RIGHT, padx=(7, 0)) + self.ttk.Button(cmdBar, text="Reset", style="Tool.TButton", command=self.resetOptions, + takefocus=False).pack(side=tk.RIGHT, padx=(7, 0)) self.command = tk.StringVar(value="sqlmap.py") cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], - bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", - disabledforeground=PALETTE["blue"], relief="sunken", borderwidth=2, - highlightthickness=0, state="readonly") - cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True) + bg=p["command"], fg=p["commandText"], readonlybackground=p["command"], + disabledforeground=p["commandText"], relief="flat", borderwidth=0, + highlightthickness=1, highlightbackground=p["border"], + highlightcolor=p["sapphire"], state="readonly") + cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=5) - hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 9)) + hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(16, 8)) hintBar.pack(fill=tk.X) + self.statusLight = tk.Canvas(hintBar, width=12, height=12, background=p["panel"], + highlightthickness=0, borderwidth=0) + self.statusLight.pack(side=tk.LEFT, padx=(0, 8)) self.stat = tk.StringVar(value="") self.ttk.Label(hintBar, textvariable=self.stat, style="Stat.TLabel", anchor="e").pack(side=tk.RIGHT, padx=(12, 0)) self.hint = tk.StringVar(value=HINT_DEFAULT) @@ -350,6 +675,7 @@ class SqlmapGui(object): for group in _parserGroups(self.parser): self._buildGroupPane(group) + self._prebuildQueue = list(self.paneOrder) self._selectPane("Quick start") self.window.bind("", lambda e: self._navKey(1)) self.window.bind("", lambda e: self._navKey(-1)) @@ -359,34 +685,14 @@ class SqlmapGui(object): self.window.bind("", lambda e: self.run()) self.window.bind("", lambda e: self.run()) self.window.bind("", lambda e: self._focusTarget()) + self.window.bind("", lambda e: self._focusSearch()) + self.window.bind("", self._escapeAction) self.window.bind("", lambda e: self.saveConfigDialog()) self.window.bind("", lambda e: self.loadConfig()) self._enableSelectAll() - self._tickStats() - self._prebuildPanes() - self._center(self.window, 1000, 720) - - def _prebuildPanes(self): - # Tk isn't thread-safe, so widgets must be built on the main thread; instead of blocking, - # build the not-yet-visited panes one per idle tick so they are ready (instant) by the time - # the user navigates to them, while the UI stays responsive (on-demand build is the fallback) - pending = [_ for _ in self.paneOrder if _ not in self.built] - - def step(): - while pending and pending[0] in self.built: - pending.pop(0) - if not pending: - return - name = pending.pop(0) - try: - self.builders[name](self.inners[name]) - self.built.add(name) - except Exception: - pass - if pending: - self.window.after(30, step) - - self.window.after(250, step) + self._refreshDerivedState() + self._center(self.window, 1060, 750) + self._schedulePanePrebuild(60) def _enableSelectAll(self): # Tk binds Ctrl-A to "cursor to line start" by default; rebind it to select-all, @@ -414,21 +720,25 @@ class SqlmapGui(object): def _buildMenu(self): p = PALETTE - menubar = self.tk.Menu(self.window, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"], borderwidth=0) - filemenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + menuKw = dict(bg=p["panel"], fg=p["text"], activebackground=p["sapphire"], + activeforeground="#ffffff") + menubar = self.tk.Menu(self.window, borderwidth=0, **menuKw) + filemenu = self.tk.Menu(menubar, tearoff=0, **menuKw) filemenu.add_command(label="Load configuration...", command=self.loadConfig) filemenu.add_command(label="Save configuration...", command=self.saveConfigDialog) + filemenu.add_command(label="Reset all options", command=self.resetOptions) filemenu.add_separator() - filemenu.add_command(label="Exit", command=self.window.quit) + filemenu.add_command(label="Exit", command=self._closeApplication) menubar.add_cascade(label="File", menu=filemenu) menubar.add_command(label="Run", command=self.run) - helpmenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + helpmenu = self.tk.Menu(menubar, tearoff=0, **menuKw) helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) helpmenu.add_command(label="GitHub", command=lambda: webbrowser.open(GIT_PAGE)) helpmenu.add_command(label="Wiki", 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: self.messagebox.showinfo("About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) + helpmenu.add_command(label="About", command=lambda: self.messagebox.showinfo( + "About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) menubar.add_cascade(label="Help", menu=helpmenu) self.window.config(menu=menubar) @@ -436,7 +746,15 @@ class SqlmapGui(object): self._runHover = False self.header = self.tk.Canvas(self.window, height=76, highlightthickness=0, borderwidth=0, background=PALETTE["base"]) self.header.pack(fill=self.tk.X) - self.header.bind("", lambda e: self._drawHeader()) + self.header.bind("", self._scheduleHeaderDraw) + + def _scheduleHeaderDraw(self, event=None): + if self._headerJob is not None: + try: + self.window.after_cancel(self._headerJob) + except Exception: + pass + self._headerJob = self.window.after(35, self._drawHeader) def _interp(self, color1, color2, ratio): a = [int(color1[_:_ + 2], 16) for _ in (1, 3, 5)] @@ -444,51 +762,87 @@ class SqlmapGui(object): return "#%02x%02x%02x" % tuple(int(a[_] + (b[_] - a[_]) * ratio) for _ in range(3)) def _drawHeader(self): + """Draw the header only for resize or process-state changes. + + Keep this deliberately cheap. Redrawing a canvas from an / + callback can remove the item currently under the pointer, which generates a + matching leave/enter pair and can turn into an event/redraw loop on Tk/X11. + """ + self._headerJob = None p = PALETTE c = self.header c.delete("all") - width = c.winfo_width() + width = max(1, c.winfo_width()) height = 76 - steps = max(1, width // 4) - for i in range(steps): - c.create_rectangle(i * width / steps, 0, (i + 1) * width / steps + 1, height, - outline="", fill=self._interp(p["blue"], p["title2"], i / float(steps))) - c.create_text(24, 27, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"]) - c.create_text(122, 31, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w", fill="#c7d8ef", font=self.fonts["subtitle"]) - c.create_text(24, 54, text="automatic SQL injection and database takeover tool", anchor="w", fill="#dfe8f6", font=self.fonts["small"]) + + # A small, fixed number of primitives paints faster and more consistently + # than a strip-per-gradient header, especially on X11 and remote displays. + c.create_rectangle(0, 0, width, height, outline="", fill="#17445f") + c.create_rectangle(0, 0, 6, height, outline="", fill=p["sky"]) + c.create_rectangle(6, height - 4, width, height, outline="", fill="#0e7697") + c.create_line(22, 64, max(22, width - 160), 64, fill="#39738a") + + c.create_text(26, 26, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"]) + c.create_text(124, 30, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w", + fill="#bfe1ed", font=self.fonts["subtitle"]) + c.create_text(26, 52, text="automatic SQL injection and database takeover tool", anchor="w", + fill="#dcecf2", font=self.fonts["small"]) self._drawRunButton(width, height) + def _isRunning(self): + return self.process is not None and self.process.poll() is None + def _drawRunButton(self, width, height): p = PALETTE c = self.header + running = self._isRunning() bw, bh = 116, 34 x0 = width - bw - 22 y0 = (height - bh) // 2 x1, y1 = x0 + bw, y0 + bh - c.create_rectangle(x0, y0, x1, y1, fill=p["base"], outline="", tags=("runbtn", "runpill")) - # classic raised 3D bevel (white top/left, dark bottom/right) - c.create_line(x0, y0, x1, y0, fill="#ffffff", tags="runbtn") - c.create_line(x0, y0, x0, y1, fill="#ffffff", tags="runbtn") - c.create_line(x0, y1, x1, y1, fill=p["dark"], tags="runbtn") - c.create_line(x1, y0, x1, y1 + 1, fill=p["dark"], tags="runbtn") - c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") - c.create_line(x1 - 1, y0 + 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + baseFill = p["red"] if running else p["success"] + fill = ("#d15056" if running else "#3bae6b") if self._runHover else baseFill + c.create_rectangle(x0, y0, x1, y1, fill=fill, outline="#d9f1e3", width=1, + tags=("runbtn", "runpill")) + c.create_line(x0 + 1, y0 + 1, x1 - 1, y0 + 1, fill="#8fd2aa" if not running else "#ef9da1", + tags="runbtn") + c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill="#17613a" if not running else "#75252a", + tags="runbtn") cy = (y0 + y1) // 2 - tx = x0 + 24 - c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill=p["blue"], outline="", tags=("runbtn", "runico")) - c.create_text((x0 + x1) // 2 + 8, cy, text="Run", fill=p["text"], font=self.fonts["bodyBold"], tags=("runbtn", "runico")) - c.tag_bind("runbtn", "", lambda e: self.run()) + tx = x0 + 23 + if running: + c.create_rectangle(tx, cy - 6, tx + 11, cy + 6, fill="#ffffff", outline="", + tags=("runbtn", "runico")) + else: + c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill="#ffffff", outline="", + tags=("runbtn", "runico")) + c.create_text((x0 + x1) // 2 + 8, cy, text=("Stop" if running else "Run"), fill="#ffffff", + font=self.fonts["bodyBold"], tags=("runbtn", "runico")) + c.tag_bind("runbtn", "", lambda e: self._runButtonAction()) c.tag_bind("runbtn", "", lambda e: self._hoverRun(True)) c.tag_bind("runbtn", "", lambda e: self._hoverRun(False)) + def _runButtonAction(self): + if self._isRunning(): + self.stopProcess() + else: + self.run() + def _hoverRun(self, on): + """Update only the existing button items; never rebuild the header here.""" self._runHover = on - self.header.itemconfigure("runpill", fill="#ccccc6" if on else PALETTE["base"]) try: + running = self._isRunning() + if on: + fill = "#d15056" if running else "#3bae6b" + else: + fill = PALETTE["red"] if running else PALETTE["success"] + self.header.itemconfigure("runpill", fill=fill) self.header.configure(cursor="hand2" if on else "") except Exception: pass + def _drawIcon(self, c, name, col): # minimal line-art icons, drawn as vectors so they render everywhere and need no assets c.delete("all") @@ -595,10 +949,10 @@ class SqlmapGui(object): icon = tk.Canvas(row, width=22, height=22, highlightthickness=0, borderwidth=0, background=p["mantle"]) icon.pack(side=tk.LEFT, padx=(13, 0), pady=8) self._drawIcon(icon, name, self._iconColor(name)) - badge = tk.Label(row, text="", background=p["mantle"], foreground=p["blue"], font=self.fonts["small"]) + badge = tk.Label(row, text="", background=p["mantle"], foreground=p["navMuted"], font=self.fonts["small"]) badge.pack(side=tk.RIGHT, padx=(0, 12)) self.badges[name] = badge - lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["subtext"], + lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["navText"], font=self.fonts["nav"], anchor="w", padx=10, pady=9) lab.pack(side=tk.LEFT, fill=tk.X, expand=True) for w in (row, lab, strip, icon, badge): @@ -609,7 +963,7 @@ class SqlmapGui(object): self.paneOrder.append(name) outer = self.ttk.Frame(self.content, style="Card.TFrame") - canvas = tk.Canvas(outer, background=p["base"], highlightthickness=0, borderwidth=0) + canvas = tk.Canvas(outer, background=p["panel"], highlightthickness=0, borderwidth=0) scrollbar = self.ttk.Scrollbar(outer, orient="vertical", command=canvas.yview, style="Vertical.TScrollbar") inner = self.ttk.Frame(canvas, style="Card.TFrame", padding=(24, 20)) inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) @@ -627,9 +981,11 @@ class SqlmapGui(object): return PALETTE.get(ICON_COLORS.get(name, "subtext"), PALETTE["subtext"]) def _navHover(self, name, entering): + if entering: + self._prioritizePaneBuild(name) if name == self.currentPane: return - bg = PALETTE["surface2"] if entering else PALETTE["mantle"] + bg = PALETTE["navHover"] if entering else PALETTE["mantle"] row, strip, icon, lab, badge = self.navItems[name] for w in (row, strip, icon, lab, badge): w.configure(background=bg) @@ -647,9 +1003,11 @@ class SqlmapGui(object): return "break" def _selectPane(self, name): - if name not in self.built: # lazy: populate the pane on first visit - self.builders[name](self.inners[name]) - self.built.add(name) + # Build only a tiny, time-bounded slice synchronously so a never-visited pane + # appears immediately. The remaining rows are completed by the idle prebuilder. + if name not in self.built: + self._prioritizePaneBuild(name) + self._buildPaneSlice(name, budgetMs=14, minimumSteps=5) if self.currentPane == name: return p = PALETTE @@ -658,18 +1016,22 @@ class SqlmapGui(object): row, strip, icon, lab, badge = self.navItems[self.currentPane] for w in (row, strip, icon): w.configure(background=p["mantle"]) - lab.configure(background=p["mantle"], foreground=p["text"], font=self.fonts["nav"]) - badge.configure(background=p["mantle"], foreground=p["blue"]) + lab.configure(background=p["mantle"], foreground=p["navText"], font=self.fonts["nav"]) + badge.configure(background=p["mantle"], foreground=p["navMuted"]) self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) self.panes[name].pack(expand=True, fill=self.tk.BOTH) row, strip, icon, lab, badge = self.navItems[name] - for w in (row, strip, icon): + for w in (row, icon): w.configure(background=p["blue"]) + strip.configure(background=p["sky"]) lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) - badge.configure(background=p["blue"], foreground="#ffffff") + badge.configure(background=p["blue"], foreground="#d9edf5") self._drawIcon(icon, name, "#ffffff") self.currentPane = name - self._ensureNavVisible(name) + # Geometry flushing here used to make first-time pane switches feel much + # slower than the widget creation itself. Sidebar visibility can be fixed + # on the next idle turn without blocking the click handler. + self.window.after_idle(lambda n=name: self._ensureNavVisible(n) if self.currentPane == n else None) if hasattr(self, "hint"): # don't leave the previous section's option hint lingering self.hint.set(HINT_DEFAULT) @@ -678,7 +1040,6 @@ class SqlmapGui(object): # scroll the sidebar so the active item stays in view (e.g. when paging with Up/Down) try: row = self.navItems[name][0] - self.nav.update_idletasks() total = self.nav.winfo_height() viewH = self.navCanvas.winfo_height() if total <= 1 or viewH <= 1: @@ -694,8 +1055,14 @@ class SqlmapGui(object): pass def _onWheel(self, event): - # route the wheel to whichever scroll region the pointer is over (sidebar or content) - delta = 1 if getattr(event, "num", None) == 5 or getattr(event, "delta", 0) < 0 else -1 + # Route the wheel only when the pointer is actually over this window's sidebar/content. + rawDelta = getattr(event, "delta", 0) + if getattr(event, "num", None) == 5: + delta = 1 + elif getattr(event, "num", None) == 4: + delta = -1 + else: + delta = -int(rawDelta / 120) if abs(rawDelta) >= 120 else (-1 if rawDelta > 0 else 1) target = None node = self.window.winfo_containing(event.x_root, event.y_root) while node is not None: @@ -709,26 +1076,248 @@ class SqlmapGui(object): node = node.master except Exception: break - if target is None and self.currentPane: - target = self.canvases.get(self.currentPane) if target is not None: target.yview_scroll(delta, "units") + return "break" + return None + + def _schedulePanePrebuild(self, delay=1): + if self._prebuildJob is not None: + return + try: + self._prebuildJob = self.window.after(delay, self._pumpPanePrebuild) + except Exception: + self._prebuildJob = None + + def _prioritizePaneBuild(self, name): + if name in self.built: + return + try: + self._prebuildQueue.remove(name) + except ValueError: + pass + self._prebuildQueue.insert(0, name) + self._schedulePanePrebuild() + + def _buildPaneSlice(self, name, budgetMs=7, minimumSteps=1): + if name in self.built: + return True + builder = self.builders.get(name) + if builder is None: + self.built.add(name) + return True + state = self.buildStates.get(name) + if state is None: + state = builder(self.inners[name]) + self.buildStates[name] = state + + deadline = _clock() + max(0.001, budgetMs / 1000.0) + steps = 0 + while steps < minimumSteps or _clock() < deadline: + try: + next(state) + steps += 1 + except StopIteration: + self.buildStates.pop(name, None) + self.built.add(name) + try: + self._prebuildQueue.remove(name) + except ValueError: + pass + return True + except Exception: + # A broken optional field should not make the whole GUI unusable. + self.buildStates.pop(name, None) + self.built.add(name) + try: + self._prebuildQueue.remove(name) + except ValueError: + pass + return True + return False + + def _pumpPanePrebuild(self): + self._prebuildJob = None + while self._prebuildQueue and self._prebuildQueue[0] in self.built: + self._prebuildQueue.pop(0) + if not self._prebuildQueue: + return + + name = self._prebuildQueue[0] + finished = self._buildPaneSlice(name, budgetMs=7, minimumSteps=1) + if finished and self._prebuildQueue and self._prebuildQueue[0] == name: + self._prebuildQueue.pop(0) + # Yield to pointer, keyboard, expose and paint events after every small slice. + self._schedulePanePrebuild(1) + + def _scheduleSearch(self, *unused): + if self._searchJob is not None: + try: + self.window.after_cancel(self._searchJob) + except Exception: + pass + self._searchJob = self.window.after(90, self._applySearch) + + def _applySearch(self): + self._searchJob = None + query = self.searchVar.get().strip().lower() + if not query: + self.searchMatches = [] + self.searchList.delete(0, self.tk.END) + self.searchList.pack_forget() + return + + tokens = query.split() + matches = [] + for dest, index, label, section, flag, haystack in self.searchIndex: + if not all(token in haystack for token in tokens): + continue + score = 0 + if dest.lower().startswith(query): + score -= 40 + if flag.lower().startswith(query) or flag.lower().startswith("--" + query): + score -= 30 + if label.lower().startswith(query): + score -= 20 + if section.lower().startswith(query): + score -= 10 + matches.append((score, index, dest, label, section)) + + matches.sort() + matches = matches[:MAX_SEARCH_RESULTS] + self.searchMatches = [item[2] for item in matches] + self.searchList.delete(0, self.tk.END) + for _, _, dest, label, section in matches: + shortSection = NAV_ALIASES.get(section, section) + self.searchList.insert(self.tk.END, "%s [%s]" % (label, shortSection)) + + if matches: + self.searchList.configure(height=min(7, len(matches))) + self.searchList.pack(fill=self.tk.X, padx=9, pady=(0, 7), before=self.navCanvas) + self.searchList.selection_clear(0, self.tk.END) + self.searchList.selection_set(0) + self.searchList.activate(0) + else: + self.searchList.insert(self.tk.END, "No matching options") + self.searchList.configure(height=1) + self.searchList.pack(fill=self.tk.X, padx=9, pady=(0, 7), before=self.navCanvas) + + def _searchMoveDown(self, event=None): + if self.searchMatches: + self.searchList.focus_set() + self.searchList.selection_clear(0, self.tk.END) + self.searchList.selection_set(0) + self.searchList.activate(0) return "break" + def _clickSearchResult(self, event): + if not self.searchMatches: + return "break" + try: + index = int(self.searchList.nearest(event.y)) + except Exception: + index = 0 + if index < 0 or index >= len(self.searchMatches): + return "break" + # Resolve the clicked row ourselves instead of depending on Listbox class + # bindings, whose selection update happens after this widget binding. + self.searchList.selection_clear(0, self.tk.END) + self.searchList.selection_set(index) + self.searchList.activate(index) + return self._activateSearchIndex(index) + + def _activateSearchResult(self, event=None): + if not self.searchMatches: + return "break" + selection = self.searchList.curselection() + index = int(selection[0]) if selection else 0 + return self._activateSearchIndex(index) + + def _activateSearchIndex(self, index): + if not self.searchMatches: + return "break" + if index < 0 or index >= len(self.searchMatches): + index = 0 + dest = self.searchMatches[index] + section = self.sectionByDest.get(dest) + self.searchVar.set("") + if section in self.panes: + self._selectPane(section) + self._focusOptionWhenReady(dest, section) + elif dest == "url": + self._focusTarget() + return "break" + + def _focusOptionWhenReady(self, dest, paneName): + # A search can jump to an option that the incremental pane builder has not + # created yet. Continue that pane in tiny slices and focus as soon as the + # requested widget exists, without blocking the click handler. + for candidatePane, candidateWidget in self.controlsByDest.get(dest, ()): + if candidatePane == paneName: + self.window.after_idle(lambda d=dest, p=paneName: self._focusOption(d, p)) + return + if paneName not in self.built: + self._prioritizePaneBuild(paneName) + self._buildPaneSlice(paneName, budgetMs=6, minimumSteps=1) + self.window.after(1, lambda d=dest, p=paneName: self._focusOptionWhenReady(d, p)) + + def _focusOption(self, dest, paneName): + candidates = self.controlsByDest.get(dest, ()) + widget = None + for candidatePane, candidateWidget in candidates: + if candidatePane == paneName: + widget = candidateWidget + break + if widget is None and candidates: + widget = candidates[0][1] + if widget is None: + return + try: + widget.focus_set() + if isinstance(widget, (self.ttk.Entry, self.ttk.Combobox)): + widget.select_range(0, "end") + canvas = self.canvases.get(paneName) + inner = self.inners.get(paneName) + if canvas is not None and inner is not None: + inner.update_idletasks() + total = max(1, inner.winfo_height()) + canvas.yview_moveto(max(0.0, min(1.0, float(widget.winfo_y() - 30) / total))) + except Exception: + pass + + def _buildPaneHeading(self, parent, title, description, optionCount): + p = PALETTE + card = self.tk.Frame(parent, background=p["surface0"], highlightthickness=1, + highlightbackground=p["border"], borderwidth=0) + card.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 16)) + accent = self.tk.Frame(card, background=self._iconColor(title), width=5) + accent.pack(side=self.tk.LEFT, fill=self.tk.Y) + content = self.ttk.Frame(card, style="PaneHeader.TFrame", padding=(14, 10, 14, 10)) + content.pack(side=self.tk.LEFT, fill=self.tk.BOTH, expand=True) + titleRow = self.ttk.Frame(content, style="PaneHeader.TFrame") + titleRow.pack(fill=self.tk.X) + self.ttk.Label(titleRow, text=title, style="Pane.TLabel").pack(side=self.tk.LEFT) + self.ttk.Label(titleRow, text="%d option%s" % (optionCount, "" if optionCount == 1 else "s"), + style="PaneCount.TLabel").pack(side=self.tk.RIGHT, pady=(6, 0)) + if description: + self.ttk.Label(content, text=description, style="PanelHint.TLabel", wraplength=690, + justify="left").pack(fill=self.tk.X, pady=(3, 0)) + def _buildQuickStartPane(self): name = "Quick start" self._addPane(name, name) self.sectionDests[name] = [_ for _ in QUICK_START_DESTS if _ in self.optionByDest] def build(inner): - self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") - self.ttk.Label(inner, text="The options people reach for most. Set the target above, tick what you want, then Run.", - style="Desc.TLabel", wraplength=640, justify="left").grid(row=1, column=0, columnspan=2, sticky="w", pady=(2, 14)) - row = 2 + description = "The options people reach for most. Set the target above, choose what you need, then Run." + self._buildPaneHeading(inner, name, description, len(self.sectionDests[name])) + yield + row = 1 for dest in QUICK_START_DESTS: option = self.optionByDest.get(dest) if option is not None: - row = self._buildFieldRow(inner, option, row) + row = self._buildFieldRow(inner, option, row, paneName=name) + yield inner.columnconfigure(1, weight=1) self.builders[name] = build @@ -739,61 +1328,50 @@ class SqlmapGui(object): self.sectionDests[title] = [_optDest(_) for _ in _groupOptions(group) if _optDest(_)] def build(inner, group=group, title=title): - self.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + self._buildPaneHeading(inner, title, _groupDescription(group), len(self.sectionDests[title])) + yield row = 1 - description = _groupDescription(group) - if description: - self.ttk.Label(inner, text=description, style="Desc.TLabel", wraplength=640, justify="left").grid( - row=row, column=0, columnspan=2, sticky="w", pady=(2, 14)) - row += 1 for option in _groupOptions(group): - row = self._buildFieldRow(inner, option, row) + row = self._buildFieldRow(inner, option, row, paneName=title) + yield inner.columnconfigure(1, weight=1) self.builders[title] = build def _destVar(self, dest, is_bool): - # one shared variable per option, so every widget that edits it (Quick start pane, - # the proper group pane, the target bar) reflects into the same value both ways + # One shared effective-value variable per option, reflected in every duplicate control. if dest not in self.vars: - self.vars[dest] = self.tk.IntVar() if is_bool else self.tk.StringVar() + var = self.tk.BooleanVar(value=False) if is_bool else self.tk.StringVar(value="") + self.vars[dest] = var + self.widgets[dest] = ("bool" if is_bool else "string", var) + try: + var.trace("w", self._onOptionChanged) + except Exception: + pass return self.vars[dest] - def _buildFieldRow(self, parent, option, row): - p = PALETTE - tk = self.tk - label = _optionLabel(option) + def _buildFieldRow(self, parent, option, row, labelText=None, paneName=None): + label = labelText or _optionLabel(option) helptext = _optHelp(option) dest = _optDest(option) + if not dest: + return row is_bool = not _optTakesValue(option) - firstSeen = dest not in self.vars - - def bindHint(widget): - widget.bind("", lambda e: self.hint.set(helptext), add="+") - widget.bind("", lambda e: self.hint.set(helptext), add="+") - widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") - widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") if is_bool: var = self._destVar(dest, True) - chk = self.ttk.Checkbutton(parent, text=label, variable=var, takefocus=True) + default = bool(defaults.get(dest)) + chk = self.ttk.Checkbutton(parent, text=label, variable=var, + onvalue=(not default), offvalue=default, takefocus=True) chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=5) - _Tooltip(chk, helptext, tk, p) - bindHint(chk) - if firstSeen: - self.widgets[dest] = ("bool", var) + self.tooltip.attach(chk, helptext) + self.controlsByDest.setdefault(dest, []).append((paneName, chk)) else: otype = _optValueType(option) var = self._destVar(dest, False) - if firstSeen: - default = defaults.get(dest) - if default not in (None, False): - var.set(default) - self.widgets[dest] = (otype, var) lab = self.ttk.Label(parent, text=label, style="Field.TLabel") lab.grid(row=row, column=0, sticky="w", padx=(0, 18), pady=6) - _Tooltip(lab, helptext, tk, p) - bindHint(lab) + self.tooltip.attach(lab, helptext) choices = _optChoices(option) if choices: widget = self.ttk.Combobox(parent, values=list(choices), state="readonly", textvariable=var) @@ -802,15 +1380,27 @@ class SqlmapGui(object): if otype in ("int", "float"): self._constrain(widget, otype) widget.grid(row=row, column=1, sticky="ew", pady=6) - _Tooltip(widget, helptext, tk, p) - bindHint(widget) + self.tooltip.attach(widget, helptext) + self.controlsByDest.setdefault(dest, []).append((paneName, widget)) return row + 1 def _constrain(self, entry, otype): - check = (lambda s: s == "" or s.replace(".", "", 1).isdigit()) if otype == "float" else (lambda s: s == "" or s.isdigit()) - vcmd = (self.window.register(lambda proposed: bool(check(proposed))), "%P") + def check(proposed): + if proposed in ("", "+", "-", ".", "+.", "-."): + return True + try: + if otype == "int": + int(proposed) + else: + float(proposed) + return True + except (TypeError, ValueError): + return False + + vcmd = (self.window.register(check), "%P") entry.configure(validate="key", validatecommand=vcmd) + # --- helpers -------------------------------------------------------- def _center(self, window, width=None, height=None): @@ -821,21 +1411,30 @@ class SqlmapGui(object): y = window.winfo_screenheight() // 2 - height // 2 window.geometry("%dx%d+%d+%d" % (width, height, x, y)) + def _isOptionSet(self, dest): + item = self.widgets.get(dest) + if item is None: + return False + otype, var = item + try: + value = var.get() + except Exception: + return False + default = defaults.get(dest) + if otype == "bool": + return bool(value) != bool(default) + if value in (None, ""): + return False + displayDefault = "" if default in (None, False) else str(default) + return str(value) != displayDefault + def _updateStats(self): - setDests = set() - for dest, (otype, var) in self.widgets.items(): - try: - if otype == "bool": - if var.get(): - setDests.add(dest) - else: - raw = var.get() - if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): - setDests.add(dest) - except Exception: - pass + setDests = set(_ for _ in self.widgets if self._isOptionSet(_)) count = len(setDests) - self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + status = "%d option%s set" % (count, "" if count == 1 else "s") + if self._isRunning(): + status += " | running" + self.stat.set(status) for name, dests in self.sectionDests.items(): badge = self.badges.get(name) if badge is not None: @@ -843,34 +1442,24 @@ class SqlmapGui(object): badge.configure(text=(str(hits) if hits else "")) def _buildCommandString(self): - parts = ["sqlmap.py"] - for dest, (otype, var) in self.widgets.items(): + argv = ["sqlmap.py"] + for dest in self.optionOrder: + if not self._isOptionSet(dest): + continue option = self.optionByDest.get(dest) - if option is None: + flag = _preferredFlag(option) if option is not None else "" + if not flag: continue - strings = _optStrings(option) - if not strings: - continue - flag = strings[0] + otype, var = self.widgets[dest] try: - if otype == "bool": - if var.get(): - parts.append(flag) - else: - raw = var.get() - if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): - value = str(raw) - if " " in value or '"' in value: - value = '"%s"' % value.replace('"', '\\"') - parts.append("%s %s" % (flag, value)) + argv.append(flag) + if otype != "bool": + argv.append(_toText(var.get())) except Exception: pass - return " ".join(parts) - - def _tickStats(self): - self._updateStats() - self.command.set(self._buildCommandString()) - self.window.after(1200, self._tickStats) + if IS_WIN: + return _list2cmdline(argv) + return " ".join(_quoteArg(_) for _ in argv) def _copyCommand(self): try: @@ -880,6 +1469,22 @@ class SqlmapGui(object): except Exception: pass + def _pasteTarget(self): + try: + value = self.window.clipboard_get() + self.vars["url"].set(_toText(value).strip()) + self.targetEntry.focus_set() + self.targetEntry.icursor("end") + except Exception: + self.hint.set("Clipboard does not contain text") + + def _clearTarget(self): + try: + self.vars["url"].set("") + self.targetEntry.focus_set() + except Exception: + pass + def _focusTarget(self): try: self.targetEntry.focus_set() @@ -888,6 +1493,24 @@ class SqlmapGui(object): pass return "break" + def _focusSearch(self): + try: + self.searchEntry.focus_set() + self.searchEntry.select_range(0, "end") + except Exception: + pass + return "break" + + def _escapeAction(self, event=None): + try: + if self.searchVar.get(): + self.searchVar.set("") + self.searchEntry.focus_set() + return "break" + except Exception: + pass + return None + def _collectConfig(self): config = {} for dest, (otype, var) in self.widgets.items(): @@ -919,12 +1542,27 @@ class SqlmapGui(object): otype, var = self.widgets[dest] try: if otype == "bool": - var.set(1 if value else 0) + var.set(bool(value)) else: var.set("" if value in (None, False) else value) except Exception: pass + def resetOptions(self): + self._suspendRefresh = True + try: + for dest, (otype, var) in self.widgets.items(): + default = defaults.get(dest) + if otype == "bool": + var.set(bool(default)) + else: + var.set("" if default in (None, False) else default) + finally: + self._suspendRefresh = False + self._refreshDerivedState() + self.hint.set("All options reset to their defaults") + + # --- actions -------------------------------------------------------- def loadConfig(self): @@ -934,18 +1572,28 @@ class SqlmapGui(object): try: from thirdparty.six.moves import configparser as _configparser parser = _configparser.ConfigParser() + parser.optionxform = str parser.read(path) + byLower = dict((_.lower(), _) for _ in self.widgets) count = 0 - for section in parser.sections(): - for name, value in parser.items(section): - if name in self.widgets: - if self.widgets[name][0] == "bool": - self._setWidgetValue(name, str(value).lower() in ("1", "true", "yes", "on")) + self._suspendRefresh = True + try: + for section in parser.sections(): + for name, value in parser.items(section): + dest = name if name in self.widgets else byLower.get(name.lower()) + if dest is None: + continue + if self.widgets[dest][0] == "bool": + self._setWidgetValue(dest, str(value).lower() in ("1", "true", "yes", "on")) else: - self._setWidgetValue(name, value) + self._setWidgetValue(dest, value) count += 1 + finally: + self._suspendRefresh = False + self._refreshDerivedState() self.hint.set("Loaded %d options from %s" % (count, os.path.basename(path))) except Exception as ex: + self._suspendRefresh = False self.messagebox.showerror("Load failed", getSafeExString(ex)) def saveConfigDialog(self): @@ -959,85 +1607,294 @@ class SqlmapGui(object): self.messagebox.showerror("Save failed", getSafeExString(ex)) def run(self): - config = self._collectConfig() - handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) - os.close(handle) - saveConfig(config, configFile) + if self._isRunning(): + self.hint.set("sqlmap is already running") + try: + self.consoleWindow.deiconify() + self.consoleWindow.lift() + except Exception: + pass + return - self.alive = True - self.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) - self.queue = _queue.Queue() + configFile = None + try: + config = self._collectConfig() + handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) + os.close(handle) + saveConfig(config, configFile) + + env = os.environ.copy() + env.setdefault("PYTHONIOENCODING", "utf-8") + proc = 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=0, + close_fds=not IS_WIN, + universal_newlines=False, + env=env, + ) + except Exception as ex: + self._cleanupConfigFile(configFile) + self.messagebox.showerror("Unable to start sqlmap", getSafeExString(ex)) + return + + self._runSerial += 1 + serial = self._runSerial + outputQueue = _queue.Queue() + self.process = proc + self.processQueue = outputQueue + self.processConfigFile = configFile def enqueue(stream, queue): - for line in iter(stream.readline, b''): - queue.put(line) - self.alive = False - stream.close() - - thread = threading.Thread(target=enqueue, args=(self.process.stdout, self.queue)) - thread.daemon = True - thread.start() - self._openConsole() - - def _openConsole(self): - p = PALETTE - tk = self.tk - top = tk.Toplevel(self.window) - top.title("sqlmap - console") - top.configure(background=p["crust"]) - frame = self.ttk.Frame(top, style="Card.TFrame", padding=10) - frame.configure(style="Card.TFrame") - frame.pack(fill=tk.BOTH, expand=True) - - text = self.scrolledtext.ScrolledText(frame, wrap=tk.WORD, bg=p["crust"], fg=p["text"], - insertbackground=p["blue"], relief="flat", borderwidth=0, - font=self.fonts["mono"], padx=12, pady=10) - text.pack(fill=tk.BOTH, expand=True) - text.focus() - lineBuffer = {"value": ""} - - def onKey(event): - if self.process: - if event.char == "\b": - lineBuffer["value"] = lineBuffer["value"][:-1] - elif event.char: - lineBuffer["value"] += event.char - - def onReturn(event): - if self.process: + try: + for line in iter(stream.readline, b""): + if not line: + break + queue.put(_toText(line)) + except Exception as ex: + queue.put("\n[console reader error: %s]\n" % getSafeExString(ex)) + finally: try: - self.process.stdin.write(("%s\n" % lineBuffer["value"].strip()).encode()) - self.process.stdin.flush() + stream.close() except Exception: pass - lineBuffer["value"] = "" - text.insert(tk.END, "\n") - return "break" + queue.put(None) - text.bind("", onKey) - text.bind("", onReturn) + thread = threading.Thread(target=enqueue, args=(proc.stdout, outputQueue)) + thread.daemon = True + thread.start() + + self.hint.set("sqlmap started") + self._scheduleHeaderDraw() + self._refreshDerivedState() + self._openConsole(proc, outputQueue, serial) + self.window.after(200, lambda: self._watchProcess(proc, serial, configFile)) + + def _watchProcess(self, proc, serial, configFile): + if proc.poll() is None: + try: + self.window.after(200, lambda: self._watchProcess(proc, serial, configFile)) + except Exception: + pass + return + + self._cleanupConfigFile(configFile) + if self.process is proc and self._runSerial == serial: + self.process = None + self.processQueue = None + self.processConfigFile = None + self._scheduleHeaderDraw() + self._refreshDerivedState() + self.hint.set("sqlmap finished with exit code %s" % proc.returncode) + + def stopProcess(self, proc=None): + proc = proc or self.process + if proc is None or proc.poll() is not None: + return + self.hint.set("Stopping sqlmap...") + try: + if self.consoleStatus is not None: + self.consoleStatus.set("Stopping...") + except Exception: + pass + try: + proc.terminate() + except Exception as ex: + self.messagebox.showerror("Unable to stop sqlmap", getSafeExString(ex)) + return + + def forceKill(): + if proc.poll() is None: + try: + proc.kill() + except Exception: + pass + + self.window.after(1800, forceKill) + + def _cleanupConfigFile(self, path): + if path: + try: + os.remove(path) + except OSError: + pass + + def _appendConsole(self, text, content, forceScroll=False): + if not content: + return + try: + atBottom = text.yview()[1] >= 0.985 + text.configure(state="normal") + text.insert(self.tk.END, content) + lineCount = int(float(text.index("end-1c").split(".")[0])) + if lineCount > MAX_CONSOLE_LINES: + text.delete("1.0", "%d.0" % (lineCount - MAX_CONSOLE_LINES)) + text.configure(state="disabled") + if forceScroll or atBottom: + text.see(self.tk.END) + except Exception: + pass + + def _openConsole(self, proc, outputQueue, serial): + p = PALETTE + tk = self.tk + try: + if self.consoleWindow is not None and self.consoleWindow.winfo_exists(): + self.consoleWindow.destroy() + except Exception: + pass + + top = tk.Toplevel(self.window) + self.consoleWindow = top + top.title("sqlmap - console") + top.configure(background=p["base"]) + + toolbar = self.ttk.Frame(top, style="Bar.TFrame", padding=(10, 8)) + toolbar.pack(fill=tk.X) + status = tk.StringVar(value="Running") + self.consoleStatus = status + self.ttk.Label(toolbar, textvariable=status, style="Stat.TLabel").pack(side=tk.LEFT) + stopButton = self.ttk.Button(toolbar, text="Stop", command=lambda: self.stopProcess(proc)) + stopButton.pack(side=tk.RIGHT, padx=(8, 0)) + self.ttk.Button(toolbar, text="Save log...", command=lambda: self._saveConsoleLog(text)).pack(side=tk.RIGHT, padx=(8, 0)) + self.ttk.Button(toolbar, text="Clear", command=lambda: self._clearConsole(text)).pack(side=tk.RIGHT) + + frame = self.ttk.Frame(top, style="Card.TFrame", padding=(10, 0, 10, 8)) + frame.pack(fill=tk.BOTH, expand=True) + text = self.scrolledtext.ScrolledText(frame, wrap=tk.NONE, bg=p["crust"], fg=p["consoleText"], + insertbackground=p["commandText"], selectbackground=p["sapphire"], + selectforeground="#ffffff", relief="sunken", borderwidth=2, + font=self.fonts["mono"], padx=12, pady=10, state="disabled") + text.pack(fill=tk.BOTH, expand=True) + self.consoleText = text + self._appendConsole(text, "$ %s\n\n" % self.command.get(), forceScroll=True) + + inputBar = self.ttk.Frame(top, style="Bar.TFrame", padding=(10, 0, 10, 10)) + inputBar.pack(fill=tk.X) + self.ttk.Label(inputBar, text="Input:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + inputVar = tk.StringVar(value="") + inputEntry = self.ttk.Entry(inputBar, textvariable=inputVar) + inputEntry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + def sendInput(event=None): + value = inputVar.get() + if proc.poll() is not None: + return "break" + try: + proc.stdin.write(_toBytes(_toText(value) + u"\n")) + proc.stdin.flush() + self._appendConsole(text, "> %s\n" % value, forceScroll=True) + inputVar.set("") + except Exception as ex: + self._appendConsole(text, "[input error: %s]\n" % getSafeExString(ex), forceScroll=True) + return "break" + + self.ttk.Button(inputBar, text="Send", command=sendInput).pack(side=tk.RIGHT, padx=(8, 0)) + inputEntry.bind("", sendInput) + inputEntry.focus_set() + + state = {"readerDone": False, "finishedShown": False} def pump(): - drained = False try: - while True: - line = self.queue.get_nowait() - text.insert(tk.END, line.decode("utf-8", errors="replace") if isinstance(line, bytes) else line) - drained = True - except _queue.Empty: - pass - if drained: - text.see(tk.END) - if self.alive or not self.queue.empty(): - top.after(80, pump) - else: - text.insert(tk.END, "\n--- process finished ---\n") - text.see(tk.END) + if not top.winfo_exists(): + return + except Exception: + return + + chunks = [] + size = 0 + for _ in range(256): + try: + item = outputQueue.get_nowait() + except _queue.Empty: + break + if item is None: + state["readerDone"] = True + break + chunks.append(item) + size += len(item) + if size >= 131072: + break + if chunks: + self._appendConsole(text, "".join(chunks)) + + finished = proc.poll() is not None and state["readerDone"] and outputQueue.empty() + if finished and not state["finishedShown"]: + state["finishedShown"] = True + code = proc.returncode + self._appendConsole(text, "\n--- process finished (exit code %s) ---\n" % code, forceScroll=True) + status.set("Finished (exit code %s)" % code) + try: + inputEntry.configure(state="disabled") + stopButton.configure(state="disabled") + except Exception: + pass + return + top.after(45 if chunks else 90, pump) + + def closeConsole(): + if proc.poll() is None: + if not self.messagebox.askyesno("Close console", "Stop the running sqlmap process and close the console?"): + return + self.stopProcess(proc) + try: + top.destroy() + except Exception: + pass + if self.consoleWindow is top: + self.consoleWindow = None + self.consoleText = None + self.consoleStatus = None + + top.protocol("WM_DELETE_WINDOW", closeConsole) + self._center(top, 920, 600) + top.after(45, pump) + + def _clearConsole(self, text): + try: + text.configure(state="normal") + text.delete("1.0", self.tk.END) + text.configure(state="disabled") + except Exception: + pass + + def _saveConsoleLog(self, text): + path = self.filedialog.asksaveasfilename(title="Save console log", defaultextension=".log", + filetypes=[("Log file", "*.log"), ("Text file", "*.txt"), ("All files", "*.*")]) + if not path: + return + try: + with io.open(path, "w", encoding="utf-8") as handle: + handle.write(_toText(text.get("1.0", "end-1c"))) + self.hint.set("Saved console log to %s" % os.path.basename(path)) + except Exception as ex: + self.messagebox.showerror("Save log failed", getSafeExString(ex)) + + def _closeApplication(self): + proc = self.process + if proc is not None and proc.poll() is None: + if not self.messagebox.askyesno("Exit sqlmap GUI", "Stop the running sqlmap process and exit?"): + return + try: + proc.terminate() + try: + _waitForProcess(proc, 1.2) + if proc.poll() is None: + proc.kill() + except Exception: + proc.kill() + except Exception: + pass + self._cleanupConfigFile(self.processConfigFile) + try: + self.window.destroy() + except Exception: + pass - self._center(top, 900, 580) - top.after(80, pump) def runGui(parser): try: