Land #4063 allow session lists
Note: the parsing for cmd_sessions needs to be revamped and DRYd up in a separate PR.bug/bundler_fix
commit
1844b3956d
|
@ -41,7 +41,7 @@ class Core
|
|||
"-v" => [ false, "List verbose fields" ],
|
||||
"-q" => [ false, "Quiet mode" ],
|
||||
"-d" => [ true, "Detach an interactive session" ],
|
||||
"-k" => [ true, "Terminate session" ],
|
||||
"-k" => [ true, "Terminate sessions by session ID and/or range" ],
|
||||
"-K" => [ false, "Terminate all sessions" ],
|
||||
"-s" => [ true, "Run a script on the session given with -i, or all"],
|
||||
"-r" => [ false, "Reset the ring buffer for the session given with -i, or all"],
|
||||
|
@ -49,7 +49,7 @@ class Core
|
|||
|
||||
@@jobs_opts = Rex::Parser::Arguments.new(
|
||||
"-h" => [ false, "Help banner." ],
|
||||
"-k" => [ true, "Terminate the specified job name." ],
|
||||
"-k" => [ true, "Terminate jobs by job ID and/or range." ],
|
||||
"-K" => [ false, "Terminate all running jobs." ],
|
||||
"-i" => [ true, "Lists detailed information about a running job."],
|
||||
"-l" => [ false, "List all running jobs." ],
|
||||
|
@ -778,9 +778,7 @@ class Core
|
|||
def cmd_jobs(*args)
|
||||
# Make the default behavior listing all jobs if there were no options
|
||||
# or the only option is the verbose flag
|
||||
if (args.length == 0 or args == ["-v"])
|
||||
args.unshift("-l")
|
||||
end
|
||||
args.unshift("-l") if args.length == 0 || args == ["-v"]
|
||||
|
||||
verbose = false
|
||||
dump_list = false
|
||||
|
@ -788,20 +786,27 @@ class Core
|
|||
job_id = nil
|
||||
|
||||
# Parse the command options
|
||||
@@jobs_opts.parse(args) { |opt, idx, val|
|
||||
@@jobs_opts.parse(args) do |opt, idx, val|
|
||||
case opt
|
||||
when "-v"
|
||||
verbose = true
|
||||
when "-l"
|
||||
dump_list = true
|
||||
|
||||
# Terminate the supplied job name
|
||||
# Terminate the supplied job ID(s)
|
||||
when "-k"
|
||||
if (not framework.jobs.has_key?(val))
|
||||
print_error("No such job")
|
||||
else
|
||||
print_line("Stopping job: #{val}...")
|
||||
framework.jobs.stop_job(val)
|
||||
job_list = build_range_array(val)
|
||||
if job_list.blank?
|
||||
print_error("Please specify valid job identifier(s)")
|
||||
return false
|
||||
end
|
||||
print_status("Stopping the following job(s): #{job_list.join(', ')}")
|
||||
job_list.map(&:to_s).each do |job|
|
||||
if framework.jobs.has_key?(job)
|
||||
print_status("Stopping job #{job}")
|
||||
framework.jobs.stop_job(job)
|
||||
else
|
||||
print_error("Invalid job identifier: #{job}")
|
||||
end
|
||||
end
|
||||
when "-K"
|
||||
print_line("Stopping all jobs...")
|
||||
|
@ -817,28 +822,28 @@ class Core
|
|||
cmd_jobs_help
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
if (dump_list)
|
||||
print("\n" + Serializer::ReadableText.dump_jobs(framework, verbose) + "\n")
|
||||
end
|
||||
if (dump_info)
|
||||
if (job_id and framework.jobs[job_id.to_s])
|
||||
|
||||
if dump_list
|
||||
print("\n#{Serializer::ReadableText.dump_jobs(framework, verbose)}\n")
|
||||
end
|
||||
if dump_info
|
||||
if job_id && framework.jobs[job_id.to_s]
|
||||
job = framework.jobs[job_id.to_s]
|
||||
mod = job.ctx[0]
|
||||
|
||||
output = "\n"
|
||||
output = '\n'
|
||||
output += "Name: #{mod.name}"
|
||||
output += ", started at #{job.start_time}" if job.start_time
|
||||
print_line(output)
|
||||
|
||||
if (mod.options.has_options?)
|
||||
show_options(mod)
|
||||
end
|
||||
show_options(mod) if mod.options.has_options?
|
||||
|
||||
if (verbose)
|
||||
if verbose
|
||||
mod_opt = Serializer::ReadableText.dump_advanced_options(mod,' ')
|
||||
print_line("\nModule advanced options:\n\n#{mod_opt}\n") if (mod_opt and mod_opt.length > 0)
|
||||
if mod_opt && mod_opt.length > 0
|
||||
print_line("\nModule advanced options:\n\n#{mod_opt}\n")
|
||||
end
|
||||
end
|
||||
else
|
||||
print_line("Invalid Job ID")
|
||||
|
@ -1563,7 +1568,11 @@ class Core
|
|||
print_line "Usage: sessions [options]"
|
||||
print_line
|
||||
print_line "Active session manipulation and interaction."
|
||||
print(@@sessions_opts.usage())
|
||||
print(@@sessions_opts.usage)
|
||||
print_line
|
||||
print_line "Many options allow specifying session ranges using commas and dashes."
|
||||
print_line "For example: sessions -s checkvm -i 1,3-5 or sessions -k 1-2,5,6"
|
||||
print_line
|
||||
end
|
||||
|
||||
#
|
||||
|
@ -1584,250 +1593,220 @@ class Core
|
|||
extra = []
|
||||
|
||||
# Parse the command options
|
||||
@@sessions_opts.parse(args) { |opt, idx, val|
|
||||
@@sessions_opts.parse(args) do |opt, idx, val|
|
||||
case opt
|
||||
when "-q"
|
||||
quiet = true
|
||||
|
||||
# Run a command on all sessions, or the session given with -i
|
||||
when "-c"
|
||||
method = 'cmd'
|
||||
if (val)
|
||||
cmds << val
|
||||
end
|
||||
|
||||
when "-v"
|
||||
verbose = true
|
||||
|
||||
# Do something with the supplied session identifier instead of
|
||||
# all sessions.
|
||||
when "-i"
|
||||
sid = val
|
||||
|
||||
# Display the list of active sessions
|
||||
when "-l"
|
||||
method = 'list'
|
||||
|
||||
when "-k"
|
||||
method = 'kill'
|
||||
sid = val if val
|
||||
if not sid
|
||||
print_error("Specify a session to kill")
|
||||
return false
|
||||
end
|
||||
|
||||
when "-K"
|
||||
method = 'killall'
|
||||
|
||||
when "-d"
|
||||
method = 'detach'
|
||||
sid = val
|
||||
|
||||
# Run a script on all meterpreter sessions
|
||||
when "-s"
|
||||
if not script
|
||||
method = 'scriptall'
|
||||
script = val
|
||||
end
|
||||
|
||||
# Upload and exec to the specific command session
|
||||
when "-u"
|
||||
method = 'upexec'
|
||||
sid = val
|
||||
|
||||
# Reset the ring buffer read pointer
|
||||
when "-r"
|
||||
reset_ring = true
|
||||
method = 'reset_ring'
|
||||
|
||||
# Display help banner
|
||||
when "-h"
|
||||
cmd_sessions_help
|
||||
return false
|
||||
else
|
||||
extra << val
|
||||
when "-q"
|
||||
quiet = true
|
||||
# Run a command on all sessions, or the session given with -i
|
||||
when "-c"
|
||||
method = 'cmd'
|
||||
cmds << val if val
|
||||
when "-v"
|
||||
verbose = true
|
||||
# Do something with the supplied session identifier instead of
|
||||
# all sessions.
|
||||
when "-i"
|
||||
sid = val
|
||||
# Display the list of active sessions
|
||||
when "-l"
|
||||
method = 'list'
|
||||
when "-k"
|
||||
method = 'kill'
|
||||
sid = val || false
|
||||
when "-K"
|
||||
method = 'killall'
|
||||
when "-d"
|
||||
method = 'detach'
|
||||
sid = val || false
|
||||
# Run a script on all meterpreter sessions
|
||||
when "-s"
|
||||
unless script
|
||||
method = 'scriptall'
|
||||
script = val
|
||||
end
|
||||
# Upload and exec to the specific command session
|
||||
when "-u"
|
||||
method = 'upexec'
|
||||
sid = val || false
|
||||
# Reset the ring buffer read pointer
|
||||
when "-r"
|
||||
reset_ring = true
|
||||
method = 'reset_ring'
|
||||
# Display help banner
|
||||
when "-h"
|
||||
cmd_sessions_help
|
||||
return false
|
||||
else
|
||||
extra << val
|
||||
end
|
||||
}
|
||||
|
||||
if sid and not framework.sessions.get(sid)
|
||||
print_error("Invalid session id")
|
||||
return false
|
||||
end
|
||||
|
||||
if method.nil? and sid
|
||||
if !method && sid
|
||||
method = 'interact'
|
||||
end
|
||||
|
||||
unless sid.nil? || method == 'interact'
|
||||
session_list = build_range_array(sid)
|
||||
if session_list.blank?
|
||||
print_error("Please specify valid session identifier(s)")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Now, perform the actual method
|
||||
case method
|
||||
|
||||
when 'cmd'
|
||||
if (cmds.length < 1)
|
||||
print_error("No command specified!")
|
||||
return false
|
||||
end
|
||||
cmds.each do |cmd|
|
||||
if sid
|
||||
sessions = [ sid ]
|
||||
else
|
||||
sessions = framework.sessions.keys.sort
|
||||
end
|
||||
sessions.each do |s|
|
||||
session = framework.sessions.get(s)
|
||||
print_status("Running '#{cmd}' on #{session.type} session #{s} (#{session.session_host})")
|
||||
|
||||
if (session.type == "meterpreter")
|
||||
# If session.sys is nil, dont even try..
|
||||
if not (session.sys)
|
||||
print_error("Session #{s} does not have stdapi loaded, skipping...")
|
||||
next
|
||||
end
|
||||
c, c_args = cmd.split(' ', 2)
|
||||
begin
|
||||
process = session.sys.process.execute(c, c_args,
|
||||
{
|
||||
'Channelized' => true,
|
||||
'Hidden' => true
|
||||
})
|
||||
rescue ::Rex::Post::Meterpreter::RequestError
|
||||
print_error("Failed: #{$!.class} #{$!}")
|
||||
end
|
||||
if process and process.channel and (data = process.channel.read)
|
||||
print_line(data)
|
||||
end
|
||||
elsif session.type == "shell"
|
||||
if (output = session.shell_command(cmd))
|
||||
print_line(output)
|
||||
end
|
||||
end
|
||||
# If the session isn't a meterpreter or shell type, it
|
||||
# could be a VNC session (which can't run commands) or
|
||||
# something custom (which we don't know how to run
|
||||
# commands on), so don't bother.
|
||||
end
|
||||
end
|
||||
|
||||
when 'kill'
|
||||
if ((session = framework.sessions.get(sid)))
|
||||
print_status("Killing session #{sid}")
|
||||
session.kill
|
||||
else
|
||||
print_error("Invalid session identifier: #{sid}")
|
||||
end
|
||||
|
||||
when 'killall'
|
||||
print_status("Killing all sessions...")
|
||||
framework.sessions.each_sorted do |s|
|
||||
if ((session = framework.sessions.get(s)))
|
||||
session.kill
|
||||
end
|
||||
end
|
||||
|
||||
when 'detach'
|
||||
if ((session = framework.sessions.get(sid)))
|
||||
print_status("Detaching session #{sid}")
|
||||
if (session.interactive?)
|
||||
session.detach()
|
||||
end
|
||||
else
|
||||
print_error("Invalid session identifier: #{sid}")
|
||||
end
|
||||
|
||||
when 'interact'
|
||||
if ((session = framework.sessions.get(sid)))
|
||||
if (session.interactive?)
|
||||
print_status("Starting interaction with #{session.name}...\n") if (quiet == false)
|
||||
|
||||
self.active_session = session
|
||||
|
||||
session.interact(driver.input.dup, driver.output)
|
||||
|
||||
self.active_session = nil
|
||||
|
||||
if (driver.input.supports_readline)
|
||||
driver.input.reset_tab_completion
|
||||
end
|
||||
|
||||
else
|
||||
print_error("Session #{sid} is non-interactive.")
|
||||
end
|
||||
else
|
||||
print_error("Invalid session identifier: #{sid}")
|
||||
end
|
||||
|
||||
when 'scriptall'
|
||||
if (script.nil?)
|
||||
print_error("No script specified!")
|
||||
return false
|
||||
end
|
||||
|
||||
script_paths = {}
|
||||
script_paths['meterpreter'] = Msf::Sessions::Meterpreter.find_script_path(script)
|
||||
script_paths['shell'] = Msf::Sessions::CommandShell.find_script_path(script)
|
||||
|
||||
when 'cmd'
|
||||
if cmds.length < 1
|
||||
print_error("No command specified!")
|
||||
return false
|
||||
end
|
||||
cmds.each do |cmd|
|
||||
if sid
|
||||
print_status("Running script #{script} on session #{sid}...")
|
||||
sessions = [ sid ]
|
||||
sessions = session_list
|
||||
else
|
||||
print_status("Running script #{script} on all sessions...")
|
||||
sessions = framework.sessions.keys.sort
|
||||
end
|
||||
|
||||
sessions.each do |s|
|
||||
if ((session = framework.sessions.get(s)))
|
||||
if (script_paths[session.type])
|
||||
print_status("Session #{s} (#{session.session_host}):")
|
||||
begin
|
||||
session.execute_file(script_paths[session.type], extra)
|
||||
rescue ::Exception => e
|
||||
log_error("Error executing script: #{e.class} #{e}")
|
||||
end
|
||||
end
|
||||
end
|
||||
if sessions.blank?
|
||||
print_error("Please specify valid session identifier(s) using -i")
|
||||
return false
|
||||
end
|
||||
sessions.each do |s|
|
||||
session = verify_session(s)
|
||||
next unless session
|
||||
print_status("Running '#{cmd}' on #{session.type} session #{s} (#{session.session_host})")
|
||||
|
||||
when 'upexec'
|
||||
session_list = build_sessions_array(sid)
|
||||
print_status("Executing 'post/multi/manage/shell_to_meterpreter' on session(s): #{session_list}")
|
||||
session_list.each do |sess|
|
||||
if ((session = framework.sessions.get(sess)))
|
||||
if (session.interactive?)
|
||||
if (session.type == "shell")
|
||||
session.init_ui(driver.input, driver.output)
|
||||
session.execute_script('post/multi/manage/shell_to_meterpreter')
|
||||
session.reset_ui
|
||||
else
|
||||
print_error("Session #{sess} is not a command shell session, skipping...")
|
||||
next
|
||||
end
|
||||
else
|
||||
print_error("Session #{sess} is non-interactive, skipping...")
|
||||
if session.type == 'meterpreter'
|
||||
# If session.sys is nil, dont even try..
|
||||
unless session.sys
|
||||
print_error("Session #{s} does not have stdapi loaded, skipping...")
|
||||
next
|
||||
end
|
||||
c, c_args = cmd.split(' ', 2)
|
||||
begin
|
||||
process = session.sys.process.execute(c, c_args,
|
||||
{
|
||||
'Channelized' => true,
|
||||
'Hidden' => true
|
||||
})
|
||||
rescue ::Rex::Post::Meterpreter::RequestError
|
||||
print_error("Failed: #{$!.class} #{$!}")
|
||||
end
|
||||
if process && process.channel
|
||||
data = process.channel.read
|
||||
print_line(data) if data
|
||||
end
|
||||
elsif session.type == 'shell'
|
||||
output = session.shell_command(cmd)
|
||||
print_line(output) if output
|
||||
end
|
||||
# If the session isn't a meterpreter or shell type, it
|
||||
# could be a VNC session (which can't run commands) or
|
||||
# something custom (which we don't know how to run
|
||||
# commands on), so don't bother.
|
||||
end
|
||||
end
|
||||
when 'kill'
|
||||
print_status("Killing the following session(s): #{session_list.join(', ')}")
|
||||
session_list.each do |sess_id|
|
||||
session = framework.sessions.get(sess_id)
|
||||
if session
|
||||
print_status("Killing session #{sess_id}")
|
||||
session.kill
|
||||
else
|
||||
print_error("Invalid session identifier: #{sess_id}")
|
||||
end
|
||||
end
|
||||
when 'killall'
|
||||
print_status("Killing all sessions...")
|
||||
framework.sessions.each_sorted do |s|
|
||||
session = framework.sessions.get(s)
|
||||
session.kill if session
|
||||
end
|
||||
when 'detach'
|
||||
print_status("Detaching the following session(s): #{session_list.join(', ')}")
|
||||
session_list.each do |sess_id|
|
||||
session = verify_session(sess_id)
|
||||
# if session is interactive, it's detachable
|
||||
if session
|
||||
print_status("Detaching session #{sess_id}")
|
||||
session.detach
|
||||
end
|
||||
end
|
||||
when 'interact'
|
||||
session = verify_session(sid)
|
||||
if session
|
||||
print_status("Starting interaction with #{session.name}...\n") unless quiet
|
||||
self.active_session = session
|
||||
session.interact(driver.input.dup, driver.output)
|
||||
self.active_session = nil
|
||||
driver.input.reset_tab_completion if driver.input.supports_readline
|
||||
end
|
||||
when 'scriptall'
|
||||
unless script
|
||||
print_error("No script specified!")
|
||||
return false
|
||||
end
|
||||
script_paths = {}
|
||||
script_paths['meterpreter'] = Msf::Sessions::Meterpreter.find_script_path(script)
|
||||
script_paths['shell'] = Msf::Sessions::CommandShell.find_script_path(script)
|
||||
|
||||
sessions = sid ? session_list : framework.sessions.keys.sort
|
||||
|
||||
sessions.each do |sess_id|
|
||||
session = verify_session(sess_id, true)
|
||||
# @TODO: Not interactive sessions can or cannot have scripts run on them?
|
||||
if session == false # specifically looking for false
|
||||
# if verify_session returned false, sess_id is valid, but not interactive
|
||||
session = framework.sessions.get(sess_id)
|
||||
end
|
||||
if session
|
||||
if script_paths[session.type]
|
||||
print_status("Session #{sess_id} (#{session.session_host}):")
|
||||
print_status("Running script #{script} on #{session.type} session" +
|
||||
" #{sess_id} (#{session.session_host})")
|
||||
begin
|
||||
session.execute_file(script_paths[session.type], extra)
|
||||
rescue ::Exception => e
|
||||
log_error("Error executing script: #{e.class} #{e}")
|
||||
end
|
||||
end
|
||||
else
|
||||
print_error("Invalid session identifier: #{sess_id}")
|
||||
end
|
||||
end
|
||||
when 'upexec'
|
||||
print_status("Executing 'post/multi/manage/shell_to_meterpreter' on " +
|
||||
"session(s): #{session_list}")
|
||||
session_list.each do |sess_id|
|
||||
session = verify_session(sess_id)
|
||||
if session
|
||||
if session.type == 'shell'
|
||||
session.init_ui(driver.input, driver.output)
|
||||
session.execute_script('post/multi/manage/shell_to_meterpreter')
|
||||
session.reset_ui
|
||||
else
|
||||
print_error("Invalid session identifier: #{sess}")
|
||||
print_error("Session #{sess_id} is not a command shell session, skipping...")
|
||||
next
|
||||
end
|
||||
|
||||
if session_list.count > 1
|
||||
print_status("Sleeping 5 seconds to allow the previous handler to finish..")
|
||||
sleep(5)
|
||||
end
|
||||
end
|
||||
|
||||
when 'reset_ring'
|
||||
sessions = sid ? [ sid ] : framework.sessions.keys
|
||||
sessions.each do |sidx|
|
||||
s = framework.sessions[sidx]
|
||||
next if not (s and s.respond_to?(:ring_seq))
|
||||
s.reset_ring_sequence
|
||||
print_status("Reset the ring buffer pointer for Session #{sidx}")
|
||||
if session_list.count > 1
|
||||
print_status("Sleeping 5 seconds to allow the previous handler to finish..")
|
||||
sleep(5)
|
||||
end
|
||||
|
||||
when 'list',nil
|
||||
print_line
|
||||
print(Serializer::ReadableText.dump_sessions(framework, :verbose => verbose))
|
||||
print_line
|
||||
end
|
||||
when 'reset_ring'
|
||||
sessions = sid ? [sid] : framework.sessions.keys
|
||||
sessions.each do |sidx|
|
||||
s = framework.sessions[sidx]
|
||||
next unless (s && s.respond_to?(:ring_seq))
|
||||
s.reset_ring_sequence
|
||||
print_status("Reset the ring buffer pointer for Session #{sidx}")
|
||||
end
|
||||
when 'list',nil
|
||||
print_line
|
||||
print(Serializer::ReadableText.dump_sessions(framework, :verbose => verbose))
|
||||
print_line
|
||||
end
|
||||
|
||||
rescue IOError, EOFError, Rex::StreamClosedError
|
||||
|
@ -1841,7 +1820,7 @@ class Core
|
|||
# Reset the active session
|
||||
self.active_session = nil
|
||||
|
||||
return true
|
||||
true
|
||||
end
|
||||
|
||||
#
|
||||
|
@ -3001,6 +2980,33 @@ class Core
|
|||
|
||||
protected
|
||||
|
||||
#
|
||||
# verifies that a given session_id is valid and that the session is interactive.
|
||||
# The various return values allow the caller to make better decisions on what
|
||||
# action can & should be taken depending on the capabilities of the session
|
||||
# and the caller's objective while making it simple to use in the nominal case
|
||||
# where the caller needs session_id to match an interactive session
|
||||
#
|
||||
# @param session_id [String] A session id, which is an integer as a string
|
||||
# @param quiet [Boolean] True means the method will produce no error messages
|
||||
# @return [session] if the given session_id is valid and session is interactive
|
||||
# @return [false] if the given session_id is valid, but not interactive
|
||||
# @return [nil] if the given session_id is not valid at all
|
||||
def verify_session(session_id, quiet = false)
|
||||
session = framework.sessions.get(session_id)
|
||||
if session
|
||||
if session.interactive?
|
||||
session
|
||||
else
|
||||
print_error("Session #{session_id} is non-interactive.") unless quiet
|
||||
false
|
||||
end
|
||||
else
|
||||
print_error("Invalid session identifier: #{session_id}") unless quiet
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Go_pro methods -- these are used to start and connect to
|
||||
# Metasploit Community / Pro.
|
||||
|
@ -3352,30 +3358,39 @@ class Core
|
|||
start = line_num - before
|
||||
start = 0 if start < 0
|
||||
finish = line_num + after
|
||||
return all_lines.slice(start..finish)
|
||||
all_lines.slice(start..finish)
|
||||
end
|
||||
|
||||
# Generate an array of session IDs when presented with input such as '1' or '1,2,4-6,10' or '1,2,4..6,10'
|
||||
def build_sessions_array(sid_list)
|
||||
session_list = Array.new
|
||||
temp_list = sid_list.split(",")
|
||||
#
|
||||
# Generate an array of job or session IDs from a given range String.
|
||||
# Always returns an Array.
|
||||
#
|
||||
# @param id_list [String] Range or list description such as 1-5 or 1,3,5 etc
|
||||
# @return [Array<String>] Representing the range
|
||||
def build_range_array(id_list)
|
||||
item_list = []
|
||||
unless id_list.blank?
|
||||
temp_list = id_list.split(',')
|
||||
temp_list.each do |ele|
|
||||
return if ele.count('-') > 1
|
||||
return if ele.first == '-' || ele[-1] == '-'
|
||||
return if ele.first == '.' || ele[-1] == '.'
|
||||
|
||||
temp_list.each do |ele|
|
||||
if ele.include? '-'
|
||||
temp_array = (ele.split("-").inject {|s,e| s.to_i..e.to_i}).to_a
|
||||
session_list.concat(temp_array)
|
||||
elsif ele.include? '..'
|
||||
temp_array = (ele.split("..").inject {|s,e| s.to_i..e.to_i}).to_a
|
||||
session_list.concat(temp_array)
|
||||
else
|
||||
session_list.push(ele.to_i)
|
||||
if ele.include? '-'
|
||||
temp_array = (ele.split("-").inject { |s, e| s.to_i..e.to_i }).to_a
|
||||
item_list.concat(temp_array)
|
||||
elsif ele.include? '..'
|
||||
temp_array = (ele.split("..").inject { |s, e| s.to_i..e.to_i }).to_a
|
||||
item_list.concat(temp_array)
|
||||
else
|
||||
item_list.push(ele.to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return session_list.uniq.sort
|
||||
item_list.uniq.sort
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
end end end end
|
||||
|
|
Loading…
Reference in New Issue