Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

refs #15 - adds basic support for shell commands

services started with :allow_shell_cmds => true will execute shell
commands when requested.

the client uses termios to fully unbuffer STDIN. the server still needs
to make stdin and stdout of the spawned process appear as a tty: vim
doesn't work yet.
  • Loading branch information...
commit d008c971681fa89ff5bf78c461ea0b5e773db35a 1 parent 889fad3
Caleb Crane authored
36 lib/pry-remote-em.rb
View
@@ -23,3 +23,39 @@ def remote_pry_em(host = PryRemoteEm::DEFHOST, port = PryRemoteEm::DEFPORT, opts
PryRemoteEm::Server.run(opts[:target], host, port, opts, &blk)
end
end
+
+
+unless defined?(EventMachine.popen3)
+ module EventMachine
+ # @see http://eventmachine.rubyforge.org/EventMachine.html#M000491
+ # @see https://gist.github.com/535644/4d5b645b96764e07ccb53539529bea9270741e1a
+ def self.popen3(cmd, handler=nil, *args)
+ klass = klass_from_handler(Connection, handler, *args)
+ w = Shellwords::shellwords(cmd)
+ w.unshift(w.first) if w.first
+
+ new_stderr = $stderr.dup
+ rd, wr = IO::pipe
+
+ $stderr.reopen wr
+ s = invoke_popen(w)
+ $stderr.reopen new_stderr
+
+ klass.new(s, *args).tap do |c|
+ EM.attach(rd, Popen3StderrHandler, c)
+ @conns[s] = c
+ yield(c) if block_given?
+ end
+ end
+
+ class Popen3StderrHandler < EventMachine::Connection
+ def initialize(connection)
+ @connection = connection
+ end
+
+ def receive_data(data)
+ @connection.receive_stderr(data)
+ end
+ end # class::Popen3StderrHandler
+ end # module::EventMachine
+end # defined?(EventMachine.popen3)
13 lib/pry-remote-em/client.rb
View
@@ -1,5 +1,6 @@
require 'uri'
require 'pry-remote-em'
+require 'pry-remote-em/client/keyboard'
require 'pry/helpers/base_helpers'
#require "readline" # doesn't work with Fiber.yield
# - /Users/caleb/src/pry-remote-em/lib/pry-remote-em/client.rb:45:in `yield': fiber called across stack rewinding barrier (FiberError)
@@ -69,6 +70,15 @@ def receive_json(j)
elsif j['mb']
Kernel.puts "\033[1m!! msg: " + j['mb'] + "\033[0m"
+ elsif j['s'] # shell command output
+ Kernel.puts j['s']
+
+ elsif j.include?('sc') # command completed
+ if @keyboard
+ @keyboard.bufferio(true)
+ @keyboard.close_connection
+ end
+
elsif j['g'] # server banner
Kernel.puts "[pry-remote-em] remote is #{j['g']}"
name, version, scheme = j['g'].split(" ", 3)
@@ -133,6 +143,9 @@ def readline(prompt)
send_data({:b => l[2..-1]})
elsif '!' == l[0]
send_data({:m => l[1..-1]})
+ elsif '.' == l[0]
+ send_data({:s => l[1..-1]})
+ @keyboard = EM.open_keyboard(Keyboard, self)
else
send_data(l)
end # "!!" == l[0..1]
37 lib/pry-remote-em/client/keyboard.rb
View
@@ -0,0 +1,37 @@
+require "termios"
+module PryRemoteEm
+ module Client
+ module Keyboard
+
+ def initialize(c)
+ @con = c
+ bufferio(false)
+ # TODO retain the old SIGINT handler and reset it later
+ trap :SIGINT do
+ @con.send_data({:ssc => true})
+ end
+ end
+
+ def receive_data(d)
+ @con.send_data({:sd => d})
+ end
+
+ def unbind
+ bufferio(true)
+ trap :SIGINT do
+ Process.exit
+ end
+ end
+
+ # Makes stdin buffered or unbuffered.
+ # In unbuffered mode read and select will not wait for "\n"; also will not echo characters.
+ # This probably does not work on Windows
+ def bufferio( enable, io = $stdin )
+ attr = Termios::getattr( io )
+ enable ? (attr.c_lflag |= Termios::ICANON | Termios::ECHO) : (attr.c_lflag &= ~(Termios::ICANON|Termios::ECHO))
+ Termios::setattr( $stdin, Termios::TCSANOW, attr )
+ end
+ end # module::Keyboard
+ end # module::Client
+end # module PryRemoteEm
+
33 lib/pry-remote-em/server.rb
View
@@ -1,6 +1,7 @@
require 'pry'
require 'logger'
require 'pry-remote-em'
+require 'pry-remote-em/server/shell_cmd'
# How it works with Pry
#
# When PryRemoteEm.run is called it registers with EventMachine for a given ip
@@ -83,9 +84,10 @@ def unregister(obj, peer)
end # class << self
def initialize(obj, opts = {:tls => false})
- @obj = obj
- @opts = opts
- @log = opts[:logger] || ::Logger.new(STDERR)
+ @obj = obj
+ @opts = opts
+ @allow_shell_cmds = opts[:allow_shell_cmds]
+ @log = opts[:logger] || ::Logger.new(STDERR)
# Blocks that will be called on each authentication attempt, prior checking the credentials
@auth_attempt_cbs = []
# All authentication attempts that occured before an auth callback was registered
@@ -119,7 +121,6 @@ def initialize(obj, opts = {:tls => false})
def post_init
@lines = []
Pry.config.pager, @old_pager = false, Pry.config.pager
- Pry.config.system, @old_system = PryRemoteEm::Server::System, Pry.config.system
@auth_required = @auth
port, ip = Socket.unpack_sockaddr_in(get_peername)
@log.info("[pry-remote-em] received client connection from #{ip}:#{port}")
@@ -170,6 +171,7 @@ def receive_json(j)
return send_data({:a => 'auth data must be a two element array'}) unless j['a'].is_a?(Array) && j['a'].length == 2
auth_attempt(j['a'][0], peer_ip)
unless (@auth_required = !@auth.call(*j['a']))
+ @user = j['a'][0]
auth_ok(j['a'][0], peer_ip)
authenticated!
else
@@ -192,6 +194,24 @@ def receive_json(j)
peers(:all).each { |peer| peer.send_bmessage(j['b']) }
send_last_prompt
+ elsif j['s'] # shell command
+ # TODO confirm shell command's are allowed
+ unless @allow_shell_cmds
+ puts "\033[1mshell commands are not allowed by this server\033[0m"
+ @log.error("refused to execute shell command '#{j['s']}' for #{@user} (#{peer_ip}:#{peer_port})")
+ send_data({:sc => -1})
+ send_last_prompt
+ else
+ @log.warn("executing shell command '#{j['s']}' for #{@user} (#{peer_ip}:#{peer_port})")
+ @shell_cmd = EM.popen3(j['s'], ShellCmd, self)
+ end
+
+ elsif j['sd'] # shell data
+ @shell_cmd.send_data(j['sd'])
+
+ elsif j['ssc'] # shell ctrl-c
+ @shell_cmd.close_connection
+
else
warn "received unexpected data: #{j.inspect}"
end # j['d']
@@ -206,7 +226,6 @@ def authenticated!
def unbind
Pry.config.pager = @old_pager
- Pry.config.system = @old_system
PryRemoteEm::Server.unregister(@obj, self)
@log.debug("[pry-remote-em] remote session terminated (#{peer_ip}:#{peer_port})")
end
@@ -301,9 +320,5 @@ def tty?
def flush
true
end
-
- System = proc do |output, cmd, _|
- output.puts("shell commands are not yet supported")
- end
end # module::Server
end # module::PryRemoteEm
20 lib/pry-remote-em/server/shell_cmd.rb
View
@@ -0,0 +1,20 @@
+module PryRemoteEm
+ module Server
+ module ShellCmd
+ def initialize(pryem)
+ @pryem = pryem
+ end
+
+ def receive_data(d)
+ @pryem.send_data({:s => d.force_encoding("utf-8") })
+ end
+
+ alias :receive_stderr :receive_data
+
+ def unbind
+ @pryem.send_data({:sc => get_status.exitstatus })
+ @pryem.send_last_prompt
+ end
+ end # module::ShellCmd
+ end # module::Server
+end # module::PryRemoteEm
1  pry-remote-em.gemspec
View
@@ -15,5 +15,6 @@ Gem::Specification.new do |s|
s.add_dependency 'eventmachine'
s.add_dependency 'pry', '~> 0.9.6'
s.add_dependency 'rb-readline'
+ s.add_dependency 'termios'
s.add_dependency 'highline'
end
4 test/service.rb
View
@@ -52,9 +52,9 @@ def weather
EM.run{
Foo.new(auth_hash)
anon_obj.new.remote_pry_em('localhost', :auto, :tls => true, :target => binding)
- anon_obj.new.remote_pry_em('localhost', :auto, :tls => true)
+ anon_obj.new.remote_pry_em('localhost', :auto, :tls => true, :allow_shell_cmds => true)
anon_obj.new.remote_pry_em('0.0.0.0', :auto, :tls => true)
- anon_obj.new.remote_pry_em('localhost', :auto, :tls => true, :auth => auth_hash) do |pry|
+ anon_obj.new.remote_pry_em('localhost', :auto, :tls => true, :allow_shell_cmds => true, :auth => auth_hash) do |pry|
auth_logger.call(pry)
end
anon_obj.new.remote_pry_em('localhost', :auto, :tls => true, :auth => auth_anon) do |pry|
Please sign in to comment.
Something went wrong with that request. Please try again.