# -*- coding: binary -*-
require 'tempfile'
require 'filesize'
require 'rex/post/meterpreter'

module Rex
module Post
module Meterpreter
module Ui

###
#
# The file system portion of the standard API extension.
#
###
class Console::CommandDispatcher::Stdapi::Fs

  Klass = Console::CommandDispatcher::Stdapi::Fs

  include Console::CommandDispatcher

  #
  # Options for the download command.
  #
  @@download_opts = Rex::Parser::Arguments.new(
    "-h" => [ false, "Help banner." ],
    "-r" => [ false, "Download recursively." ])
  #
  # Options for the upload command.
  #
  @@upload_opts = Rex::Parser::Arguments.new(
    "-h" => [ false, "Help banner." ],
    "-r" => [ false, "Upload recursively." ])
  #
  # Options for the ls command
  #
  @@ls_opts = Rex::Parser::Arguments.new(
    "-h" => [ false, "Help banner." ],
    "-S" => [ true, "Search string." ],
    "-t" => [ false, "Sort by time" ],
    "-s" => [ false, "Sort by size" ],
    "-r" => [ false, "Reverse sort order" ],
    "-x" => [ false, "Show short file names" ],
    "-l" => [ false, "List in long format (default)" ],
    "-R" => [ false, "Recursively list subdirectories encountered" ])

  #
  # List of supported commands.
  #
  def commands
    all = {
      'cat'        => 'Read the contents of a file to the screen',
      'cd'         => 'Change directory',
      'del'        => 'Delete the specified file',
      'dir'        => 'List files (alias for ls)',
      'download'   => 'Download a file or directory',
      'edit'       => 'Edit a file',
      'getlwd'     => 'Print local working directory',
      'getwd'      => 'Print working directory',
      'lcd'        => 'Change local working directory',
      'lpwd'       => 'Print local working directory',
      'ls'         => 'List files',
      'mkdir'      => 'Make directory',
      'pwd'        => 'Print working directory',
      'rm'         => 'Delete the specified file',
      'mv'	       => 'Move source to destination',
      'rmdir'      => 'Remove directory',
      'search'     => 'Search for files',
      'upload'     => 'Upload a file or directory',
      'show_mount' => 'List all mount points/logical drives',
    }

    reqs = {
      'cat'        => [],
      'cd'         => ['stdapi_fs_chdir'],
      'del'        => ['stdapi_fs_rm'],
      'dir'        => ['stdapi_fs_stat', 'stdapi_fs_ls'],
      'download'   => [],
      'edit'       => [],
      'getlwd'     => [],
      'getwd'      => ['stdapi_fs_getwd'],
      'lcd'        => [],
      'lpwd'       => [],
      'ls'         => ['stdapi_fs_stat', 'stdapi_fs_ls'],
      'mkdir'      => ['stdapi_fs_mkdir'],
      'pwd'        => ['stdapi_fs_getwd'],
      'rmdir'      => ['stdapi_fs_delete_dir'],
      'rm'         => ['stdapi_fs_delete_file'],
      'mv'         => ['stdapi_fs_file_move'],
      'search'     => ['stdapi_fs_search'],
      'upload'     => [],
      'show_mount' => ['stdapi_fs_mount_show'],
    }

    all.delete_if do |cmd, desc|
      del = false
      reqs[cmd].each do |req|
        next if client.commands.include? req
        del = true
        break
      end

      del
    end

    all
  end

  #
  # Name for this dispatcher.
  #
  def name
    "Stdapi: File system"
  end

  #
  # Search for files.
  #
  def cmd_search(*args)

    root    = nil
    recurse = true
    globs   = []
    files   = []

    opts = Rex::Parser::Arguments.new(
      "-h" => [ false, "Help Banner." ],
      "-d" => [ true,  "The directory/drive to begin searching from. Leave empty to search all drives. (Default: #{root})" ],
      "-f" => [ true,  "A file pattern glob to search for. (e.g. *secret*.doc?)" ],
      "-r" => [ true,  "Recursivly search sub directories. (Default: #{recurse})" ]
    )

    opts.parse(args) { | opt, idx, val |
      case opt
        when "-h"
          print_line("Usage: search [-d dir] [-r recurse] -f pattern [-f pattern]...")
          print_line("Search for files.")
          print_line(opts.usage)
          return
        when "-d"
          root = val
        when "-f"
          globs << val
        when "-r"
          recurse = false if val =~ /^(f|n|0)/i
      end
    }

    if globs.empty?
      print_error("You must specify a valid file glob to search for, e.g. >search -f *.doc")
      return
    end

    globs.uniq.each do |glob|
      files += client.fs.file.search(root, glob, recurse)
    end

    if files.empty?
      print_line("No files matching your search were found.")
      return
    end

    print_line("Found #{files.length} result#{ files.length > 1 ? 's' : '' }...")
    files.each do | file |
      if file['size'] > 0
        print("    #{file['path']}#{ file['path'].empty? ? '' : '\\' }#{file['name']} (#{file['size']} bytes)\n")
      else
        print("    #{file['path']}#{ file['path'].empty? ? '' : '\\' }#{file['name']}\n")
      end
    end

  end

  #
  # Show all the mount points/logical drives (currently geared towards
  # the Windows Meterpreter).
  #
  def cmd_show_mount(*args)
    if args.include?('-h')
      print_line('Usage: show_mount')
      return true
    end

    mounts = client.fs.mount.show_mount

    table = Rex::Ui::Text::Table.new(
      'Header'    => 'Mounts / Drives',
      'Indent'    => 0,
      'SortIndex' => 0,
      'Columns'   => [
        'Name', 'Type', 'Size (Total)', 'Size (Free)', 'Mapped to'
      ]
    )

    mounts.each do |d|
      ts = ::Filesize.from("#{d[:total_space]} B").pretty.split(' ')
      fs = ::Filesize.from("#{d[:free_space]} B").pretty.split(' ')
      table << [
        d[:name],
        d[:type],
        "#{ts[0].rjust(6)} #{ts[1].ljust(3)}",
        "#{fs[0].rjust(6)} #{fs[1].ljust(3)}",
        d[:unc]
      ]
    end

    print_line
    print_line(table.to_s)
    print_line
    print_line("Total mounts/drives: #{mounts.length}")
    print_line
  end

  #
  # Reads the contents of a file and prints them to the screen.
  #
  def cmd_cat(*args)
    if (args.length == 0)
      print_line("Usage: cat file")
      return true
    end

    if (client.fs.file.stat(args[0]).directory?)
      print_error("#{args[0]} is a directory")
    else
      fd = client.fs.file.new(args[0], "rb")
      begin
        until fd.eof?
          print(fd.read)
        end
      # EOFError is raised if file is empty, do nothing, just catch
      rescue EOFError
      end
      fd.close
    end

    true
  end

  #
  # Change the working directory.
  #
  def cmd_cd(*args)
    if (args.length == 0)
      print_line("Usage: cd directory")
      return true
    end
    if args[0] =~ /\%(\w*)\%/
      client.fs.dir.chdir(client.fs.file.expand_path(args[0].upcase))
    else
      client.fs.dir.chdir(args[0])
    end

    return true
  end

  #
  # Change the local working directory.
  #
  def cmd_lcd(*args)
    if (args.length == 0)
      print_line("Usage: lcd directory")
      return true
    end

    ::Dir.chdir(args[0])

    return true
  end

  #
  # Delete the specified file.
  #
  def cmd_rm(*args)
    if (args.length == 0)
      print_line("Usage: rm file")
      return true
    end

    client.fs.file.rm(args[0])

    return true
  end

  alias :cmd_del :cmd_rm

  #
  # Move source to destination
  #
  def cmd_mv(*args)
    if (args.length < 2)
      print_line("Usage: mv oldfile newfile")
      return true
    end
    client.fs.file.mv(args[0],args[1])
    return true
  end

  alias :cmd_move :cmd_mv
  alias :cmd_rename :cmd_mv

  #
  # Move source to destination
  #
  def cmd_cp(*args)
    if (args.length < 2)
      print_line("Usage: cp oldfile newfile")
      return true
    end
    client.fs.file.cp(args[0],args[1])
    return true
  end

  alias :cmd_copy :cmd_cp


  def cmd_download_help
    print_line("Usage: download [options] src1 src2 src3 ... destination")
    print_line
    print_line("Downloads remote files and directories to the local machine.")
    print_line(@@download_opts.usage)
  end

  #
  # Downloads a file or directory from the remote machine to the local
  # machine.
  #
  def cmd_download(*args)
    if (args.empty? or args.include? "-h")
      cmd_download_help
      return true
    end

    recursive = false
    src_items = []
    last      = nil
    dest      = nil

    @@download_opts.parse(args) { |opt, idx, val|
      case opt
      when "-r"
        recursive = true
      when nil
        src_items << last if (last)
        last = val
      end
    }

    # No files given, nothing to do
    if not last
      cmd_download_help
      return true
    end

    # Source and destination will be the same
    if src_items.empty?
      src_items << last
      # Use the basename of the remote filename so we don't end up with
      # a file named c:\\boot.ini in linux
      dest = ::Rex::Post::Meterpreter::Extensions::Stdapi::Fs::File.basename(last)
    else
      dest = last
    end

    # Download to a directory, not a pattern
    if client.fs.file.is_glob?(dest)
      dest = ::File.dirname(dest)
    end

    # Go through each source item and download them
    src_items.each { |src|
      glob = nil
      if client.fs.file.is_glob?(src)
        glob = ::File.basename(src)
        src = ::File.dirname(src)
      end

      # Use search if possible for recursive pattern matching. It will work
      # more intuitively since it will not try to match on intermediate
      # directories, only file names.
      if glob && recursive && client.commands.include?('stdapi_fs_search')

        files = client.fs.file.search(src, glob, recursive)
        if !files.empty?
          print_line("Downloading #{files.length} file#{files.length > 1 ? 's' : ''}...")

          files.each do |file|
            src_separator = client.fs.file.separator
            src_path = file['path'] + client.fs.file.separator + file['name']
            dest_path = src_path.tr(src_separator, ::File::SEPARATOR)

            client.fs.file.download(dest_path, src_path) do |step, src, dst|
              puts step
              print_status("#{step.ljust(11)}: #{src} -> #{dst}")
              client.framework.events.on_session_download(client, src, dest) if msf_loaded?
            end
          end

        else
          print_status("No matching files found for download")
        end

      else
        # Perform direct matching
        stat = client.fs.file.stat(src)
        if (stat.directory?)
          client.fs.dir.download(dest, src, recursive, true, glob) do |step, src, dst|
            print_status("#{step.ljust(11)}: #{src} -> #{dst}")
            client.framework.events.on_session_download(client, src, dest) if msf_loaded?
          end
        elsif (stat.file?)
          client.fs.file.download(dest, src) do |step, src, dst|
            print_status("#{step.ljust(11)}: #{src} -> #{dst}")
            client.framework.events.on_session_download(client, src, dest) if msf_loaded?
          end
        end
      end
    }

    true
  end

  #
  # Downloads a file to a temporary file, spawns and editor, and then uploads
  # the contents to the remote machine after completion.
  #
  def cmd_edit(*args)
    if (args.length == 0)
      print_line("Usage: edit file")
      return true
    end

    # Get a temporary file path
    meterp_temp = Tempfile.new('meterp')
    meterp_temp.binmode
    temp_path = meterp_temp.path

    # Try to download the file, but don't worry if it doesn't exist
    client.fs.file.download_file(temp_path, args[0]) rescue nil

    # Spawn the editor (default to vi)
    editor = Rex::Compat.getenv('EDITOR') || 'vi'

    # If it succeeds, upload it to the remote side.
    if (system("#{editor} #{temp_path}") == true)
      client.fs.file.upload_file(args[0], temp_path)
    end

    # Get rid of that pesky temporary file
    ::File.delete(temp_path) rescue nil
  end

  #
  # Display the local working directory.
  #
  def cmd_lpwd(*args)
    print_line(::Dir.pwd)
    return true
  end

  alias cmd_getlwd cmd_lpwd


  def cmd_ls_help
    print_line "Usage: ls [options]"
    print_line
    print_line "Lists contents of directory or file info, searchable"
    print_line @@ls_opts.usage
  end

  def list_path(path, columns, sort, order, short, recursive = false, depth = 0, search_term = nil)

    # avoid infinite recursion
    if depth > 100
      return
    end

    tbl = Rex::Ui::Text::Table.new(
      'Header'  => "Listing: #{path}",
      'SortIndex' => columns.index(sort),
      'SortOrder' => order,
      'Columns' => columns,
      'SearchTerm' => search_term)

    items = 0

    # Enumerate each item...
    # No need to sort as Table will do it for us
    client.fs.dir.entries_with_info(path).each do |p|

      ffstat = p['StatBuf']
      fname = p['FileName'] || 'unknown'

      row = [
          ffstat ? ffstat.prettymode : '',
          ffstat ? ffstat.size       : '',
          ffstat ? ffstat.ftype[0,3] : '',
          ffstat ? ffstat.mtime      : '',
          fname
        ]
      row.insert(4, p['FileShortName'] || '') if short

      if fname != '.' && fname != '..'
        if row.join(' ') =~ /#{search_term}/
          tbl << row
          items += 1
        end

        if recursive && ffstat && ffstat.directory?
          if client.fs.file.is_glob?(path)
            child_path = ::File.dirname(path) + ::File::SEPARATOR + fname
            child_path += ::File::SEPARATOR + ::File.basename(path)
          else
            child_path = path + ::File::SEPARATOR + fname
          end
          begin
            list_path(child_path, columns, sort, order, short, recursive, depth + 1, search_term)
          rescue RequestError
          end
        end
      end
    end

    if items > 0
      print_line(tbl.to_s)
    else
      print_line("No entries exist in #{path}")
    end
  end

  #
  # Lists files
  #
  def cmd_ls(*args)
    # Set defaults
    path = client.fs.dir.getwd
    search_term = nil
    sort = 'Name'
    short = nil
    order = :forward
    recursive = nil

    # Parse the args
    @@ls_opts.parse(args) { |opt, idx, val|
      case opt
      # Sort options
      when '-s'
        sort = 'Size'
      when '-t'
        sort = 'Last modified'
      # Output options
      when '-x'
        short = true
      when '-l'
        short = nil
      when '-r'
        order = :reverse
      when '-R'
        recursive = true
      # Search
      when '-S'
        search_term = val
        if search_term.nil?
          print_error("Enter a search term")
          return true
        else
          search_term = /#{search_term}/nmi
        end
      # Help and path
      when "-h"
        cmd_ls_help
        return 0
      when nil
        path = val
      end
    }

    columns = [ 'Mode', 'Size', 'Type', 'Last modified', 'Name' ]
    columns.insert(4, 'Short Name') if short

    stat_path = path

    # Check session capabilities
    is_glob = client.fs.file.is_glob?(path)
    if is_glob
      if !client.commands.include?('stdapi_fs_search')
        print_line('File globbing not supported with this session')
        return
      end
      stat_path = ::File.dirname(path)
    end

    stat = client.fs.file.stat(stat_path)
    if stat.directory?
      list_path(path, columns, sort, order, short, recursive, 0, search_term)
    else
      print_line("#{stat.prettymode}  #{stat.size}  #{stat.ftype[0,3]}  #{stat.mtime}  #{path}")
    end

    return true
  end

  #
  # Alias the ls command to dir, for those of us who have windows muscle-memory
  #
  alias cmd_dir cmd_ls


  #
  # Make one or more directory.
  #
  def cmd_mkdir(*args)
    if (args.length == 0)
      print_line("Usage: mkdir dir1 dir2 dir3 ...")
      return true
    end

    args.each { |dir|
      print_line("Creating directory: #{dir}")

      client.fs.dir.mkdir(dir)
    }

    return true
  end

  #
  # Display the working directory.
  #
  def cmd_pwd(*args)
    print_line(client.fs.dir.getwd)
  end

  alias cmd_getwd cmd_pwd

  #
  # Removes one or more directory if it's empty.
  #
  def cmd_rmdir(*args)
    if (args.length == 0 or args.include?("-h"))
      print_line("Usage: rmdir dir1 dir2 dir3 ...")
      return true
    end

    args.each { |dir|
      print_line("Removing directory: #{dir}")
      client.fs.dir.rmdir(dir)
    }

    return true
  end

  def cmd_upload_help
    print_line("Usage: upload [options] src1 src2 src3 ... destination")
    print_line
    print_line("Uploads local files and directories to the remote machine.")
    print_line(@@upload_opts.usage)
  end

  #
  # Uploads a file or directory to the remote machine from the local
  # machine.
  #
  def cmd_upload(*args)
    if (args.empty? or args.include?("-h"))
      cmd_upload_help
      return true
    end

    recursive = false
    src_items = []
    last      = nil
    dest      = nil

    @@upload_opts.parse(args) { |opt, idx, val|
      case opt
        when "-r"
          recursive = true
        when nil
          if (last)
            src_items << last
          end

          last = val
      end
    }

    return true if not last

    # Source and destination will be the same
    src_items << last if src_items.empty?

    dest = last

    # Go through each source item and upload them
    src_items.each { |src|
      stat = ::File.stat(src)

      if (stat.directory?)
        client.fs.dir.upload(dest, src, recursive) { |step, src, dst|
          print_status("#{step.ljust(11)}: #{src} -> #{dst}")
          client.framework.events.on_session_upload(client, src, dest) if msf_loaded?
        }
      elsif (stat.file?)
        if client.fs.file.exists?(dest) and client.fs.file.stat(dest).directory?
          client.fs.file.upload(dest, src) { |step, src, dst|
            print_status("#{step.ljust(11)}: #{src} -> #{dst}")
            client.framework.events.on_session_upload(client, src, dest) if msf_loaded?
          }
        else
          client.fs.file.upload_file(dest, src) { |step, src, dst|
            print_status("#{step.ljust(11)}: #{src} -> #{dst}")
            client.framework.events.on_session_upload(client, src, dest) if msf_loaded?
          }
        end
      end
    }

    return true
  end

  def cmd_upload_tabs(str, words)
    return [] if words.length > 1

    tab_complete_filenames(str, words)
  end

end

end
end
end
end
