##
# swindow.rb
# By Ron Bowes
# September, 2015
#
# See LICENSE.md
#
# This implements a fairly simple multi-window buffer.
#
# When included, a thread is created that will listen to stdin and feed the
# input to whichever window is active.
#
# New instances of this class are created to create new windows. The window can
# be switched by calling the activate() or deactivate() functions.
#
# Windows are set up like a tree - when you create a window, you can specify a
# 'parent'. When a window is deactivated or closed, the parent is activated (if
# possible). Typically, you'll want one "master" window, which is the top-most
# window in the tree.
#
# User input is handled by a callback function. The proc that handles user
# input is passed to the on_input() function (which allows it to be changed),
# and it's called each time the user presses <enter>.
#
# The window can be printed to using fairly normal functions - puts, printf,
# print, etc.
#
# Windows are assigned an incremental ID value, and can be referred to as such.
#
# If you want a message to go to a window's parents (or children), a special
# function called with() can be used with a block:
#
# window.with({:to_parent => true}) do
# window.puts("hi")
# end
#
# The following options can be set:
# * :to_parent - sends to the current window and its parent
# * :to_ancestors - sends to the current window, its parent, its parent's parent, etc.
# * :to_children - Sends to the current window, and each of its children
# * :to_descendants - Sends to the current window, its children, its children's children, etc.
#
# Each window also maintains a history of typed comments, up to 1000 lines (by default).
##
require 'readline'
require 'libs/ring_buffer'
class SWindow
attr_accessor :prompt, :name, :noinput
attr_reader :id
@@id = -1
@@active = nil
@@windows = {}
@@history_size = 1000
@@firehose = false
# This function will trap the TSTP signal (suspend, ctrl-z) and, if possible,
# activate the parent window.
def SWindow._catch_suspend()
orig_suspend = Signal.trap("TSTP") do
if(@@active)
@@active.deactivate()
end
end
proc.call()
Signal.trap("TSTP", orig_suspend)
end
@@input_thread = Thread.new() do
begin
# This lets the program load a bit before the initial prompt is printed (a slightly better user experience)
sleep(0.1)
_catch_suspend() do
loop do
begin
while @@active.nil? do
end
if(@@active.noinput)
str = Readline::readline()
else
str = Readline::readline(@@active.prompt, true)
end
# If readline() returns nil, it means the input stream is closed
# (either the file it's reading from is done, or the user pressed
# ctrl-d). Terminate the input thread.
if(str.nil?)
break
end
if(@@active.nil?)
$stderr.puts("WARNING: there is no active window! Input's going nowhere")
$stderr.puts("If you think this might be a bug, please report to")
$stderr.puts("https://github.com/iagox86/dnscat2/issues")
next
end
@@active._incoming(str)
rescue SystemExit
# If something sent an exit request, we want to break, which shuts
# down the thread
break
rescue Exception => e
$stderr.puts("Something bad just happened! You will likely want to report this to")
$stderr.puts("https://github.com/iagox86/dnscat2/issues")
$stderr.puts(e.inspect)
$stderr.puts(e.backtrace.join("\n"))
end
end
end
$stderr.puts("Input thread is over")
rescue StandardError => e
$stderr.puts(e)
$stderr.puts(e.backtrace.join("\n"))
end
end
# Create a new window, with the given parent (use 'nil' for a top-level
# window, though you should try to only do one of those). Optionally, the
# window can also be activated (which means it's brought to the front).
def initialize(parent = nil, activate = false, params = {})
@parent = parent
@children = []
@id = params[:id] || (@@id += 1)
@name = params[:name] || "unnamed"
@prompt = params[:prompt] || ("%s %s> " % [@name, @id.to_s()])
@noinput = params[:noinput] || false
@times_out = params[:times_out] || false
@callback = nil
@history = RingBuffer.new(@@history_size)
@typed_history = []
@closed = false
@pending = false
@to_parent = false
@to_ancestors = false
@to_children = false
@to_descendants = false
if(@parent)
@parent._add_child(self)
end
if(@@active.nil? || activate)
self.activate()
end
if(params[:quiet] != true)
target = @parent ? @parent : self
target.with({:to_descendants => true, :to_ancestors => true}) do
target.puts("New window created: %s" % @id.to_s())
end
end
@@windows[@id.to_s()] = self
end
def _we_just_got_data()
if(@@active == self)
return
end
@pending = true
end
# Yields for each child
def children()
@children.each do |child|
yield child
end
end
# Set the on_input callback - the function that will be called when input is
# received. Very important!
def on_input()
@callback = proc
end
def with(params = {})
# Save the state
to_parent = @to_parent
to_ancestors = @to_ancestors
to_children = @to_children
to_descendants = @to_descendants
# Set the state
@to_parent = params[:to_parent] || @to_parent
@to_ancestors = params[:to_ancestors] || @to_ancestors
@to_children = params[:to_children] || @to_children
@to_descendants = params[:to_descendants] || @to_descendants
yield()
# Restore the state
@to_parent = to_parent
@to_ancestors = to_ancestors
@to_children = to_children
@to_descendants = to_descendants
end
def do_recursion(func, *args)
if(@parent && (@to_parent || @to_ancestors))
@parent.with({:to_parent => false, :to_children => false, :to_descendants => false, :to_ancestors => @to_ancestors}) do
@parent.send(func, *args)
end
end
if(@to_children || @to_descendants)
@children.each do |c|
c.with({:to_descendants => @to_descendants, :to_children => false, :to_parent => false, :to_ancestors => false}) do
c.send(func, *args)
end
end
end
end
# Write to a window, just like $stdout.puts()
def puts(str = "")
if(@@firehose)
$stdout.puts(str)
return
end
_we_just_got_data()
if(@@active == self)
$stdout.puts(str)
end
@history << (str.to_s() + "\n")
do_recursion(:puts, str)
end
# Write to a window, just like $stdout.print()
def print(str = "")
if(@@firehose)
$stdout.print(str)
return
end
_we_just_got_data()
str = str.to_s()
if(@@active == self)
$stdout.print(str)
end
@history << str.to_s()
do_recursion(:print, str)
end
# Write to a window, just like $stdout.printf()
def printf(*args)
print(sprintf(*args))
end
def _add_child(child)
@children << child
end
# Enable a window; re-draws the history, and starts sending user input to
# the specified window (note that this can be a closed window; we don't
# really care)
def activate()
# The user just viewed the window, so data is no longer pending
@pending = false
# Set this window to the activate one
@@active = self
# Re-draw the history
$stdout.puts(@history.join(""))
# It appears that some versions of Readline don't support :clear, so only do this if we can
if(Readline::HISTORY.respond_to?(:clear))
# Fill Readline's buffer with the typed history (this is a bit of a hack,
# but Readline doesn't support multiple history buffers)
Readline::HISTORY.clear()
end
@typed_history.each do |i|
Readline::HISTORY << i
end
end
# Basically, this activates the parent window (if possible)
def deactivate()
if(@parent)
@parent.activate()
else
$stdout.puts("Can't close the main window!")
end
end
def _incoming(str)
if(@noinput)
return
end
@history << @prompt + str + "\n"
if(str != '')
@typed_history << str
end
if(@callback.nil?)
self.puts("Input received, but nothing has registered to receive it")
self.puts("Use ctrl-z to escape if this window isn't taking input!")
return
end
@callback.call(str)
end
# Process some string as if it was coming from the keyboard (this can be used to,
# for example, write scripts)
def fake_input(str)
return _incoming(str)
end
# Set the number of lines of history for the current session. Note that this
# only takes effect after another message is added to the history (lazy
# evaluated, essentially).
def history_size=(size)
@history.max_size = size
end
# Get the number of lines of history for the current session.
def history_size()
return @history.max_size
end
# Set the default history size for new windows that are created. The history
# size for current windows doesn't change.
def SWindow.history_size=(size)
@@history_size = size
end
# Get the default history size.
def SWindow.history_size()
return @@history_size
end
# close the window - closing windows is purely a UI thing, they are still
# available and can receive data like anything else.
def close()
@closed = true
deactivate()
end
# Check if the window has been closed
def closed?()
return @closed
end
# Check if the window has any pending data
def pending?()
return @pending
end
# Check if a window with the given id exists
def SWindow.exists?(id)
return !@@windows[id.to_s()].nil?
end
# Retrieve a window by its id value
def SWindow.get(id)
return @@windows[id.to_s()]
end
# This function blocks until SWindow is totally finished (that is, it has
# received an exit signal or an EOF marker).
def SWindow.wait()
@@input_thread.join()
end
# This is mostly for debugging - all output goes to the same place
def SWindow.set_firehose(value)
@@firehose = value
end
def kick()
@last_seen = Time.now()
end
def to_s()
s = "%s :: %s" % [@id.to_s(), @name]
if(@@active == self)
s += " [active]"
end
if(@pending)
s += " [*]"
end
if(@times_out)
elapsed = Time.now() - @last_seen
if(elapsed > 5)
s += " [idle for #{elapsed.to_i()} seconds]"
end
end
return s
end
end