# -*- coding: binary -*-
require 'tempfile'
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." ])

	#
	# 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",
			"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",
			"rmdir"    => "Remove directory",
			"search"   => "Search for files",
			"upload"   => "Upload a file or directory",
		}

		reqs = {
			"cat"      => [ ],
			"cd"       => [ "stdapi_fs_chdir" ],
			"del"      => [ "stdapi_fs_rm" ],
			"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" ],
			"search"   => [ "stdapi_fs_search" ],
			"upload"   => [ ],
		}

		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
		glob    = nil
		recurse = true

		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,  "The 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" )
					print_line( "Search for files." )
					print_line( opts.usage )
					return
				when "-d"
					root = val
				when "-f"
					glob = val
				when "-r"
					recurse = false if( val =~ /^(f|n|0)/i )
			end
		}

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

		files = client.fs.file.search( root, glob, recurse )

		if( not files.empty? )
			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
		else
			print_line( "No files matching your search were found." )
		end

	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")

			until fd.eof?
				print(fd.read)
			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

	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

		# Go through each source item and download them
		src_items.each { |src|
			stat = client.fs.file.stat(src)

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

		return 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

		begin
			# Download the remote file to the temporary file
			client.fs.file.download_file(temp_path, args[0])
		rescue RequestError => re
			# If the file doesn't exist, then it's okay.  Otherwise, throw the
			# error.
			if re.result != 2
				raise $!
			end
		end

		# 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
		temp_path.close(true)
	end

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

	alias cmd_getlwd cmd_lpwd

	#
	# Lists files
	#
	# TODO: make this more useful
	#
	def cmd_ls(*args)
		path = args[0] || client.fs.dir.getwd
		tbl  = Rex::Ui::Text::Table.new(
			'Header'  => "Listing: #{path}",
			'SortIndex' => 4,
			'Columns' =>
				[
					'Mode',
					'Size',
					'Type',
					'Last modified',
					'Name',
				])

		items = 0
		stat = client.fs.file.stat(path)
		if stat.directory?
			# Enumerate each item...
			# No need to sort as Table will do it for us
			client.fs.dir.entries_with_info(path).each { |p|

				tbl <<
					[
						p['StatBuf'] ? p['StatBuf'].prettymode : '',
						p['StatBuf'] ? p['StatBuf'].size       : '',
						p['StatBuf'] ? p['StatBuf'].ftype[0,3] : '',
						p['StatBuf'] ? p['StatBuf'].mtime      : '',
						p['FileName'] || 'unknown'
					]

				items += 1
			}

			if (items > 0)
				print("\n" + tbl.to_s + "\n")
			else
				print_line("No entries exist in #{path}")
			end
		else
			print_line("#{stat.prettymode}  #{stat.size}  #{stat.ftype[0,3]}  #{stat.mtime}  #{path}")
		end

		return true
	end

	#
	# 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?)
				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?
				}
			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
