pry-remote-em 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -312,11 +312,65 @@ process.
312
312
 
313
313
  Message will not be displayed by the clients until the presses enter.
314
314
 
315
+ ## Authentication Event Callbacks
316
+ Available events are:
317
+
318
+ - auth_attempt - called each time authentication is attempted
319
+ - auth_fail - called each time authentication fails
320
+ - auth_ok - called each time authentication succeeds
321
+
322
+ ```ruby
323
+ log = ::Logger.new('/var/log/auth.pry.log')
324
+ obj.new.remote_pry_em('0.0.0.0', :auto, :tls => true, :auth => auth_hash) do |pry|
325
+ pry.auth_attempt do |user, ip|
326
+ log.info("got an authentication attempt for #{user} from #{ip}")
327
+ end
328
+ pry.auth_fail do |user, ip|
329
+ log.fatal("failed authentication attempt for #{user} from #{ip}")
330
+ end
331
+ pry.auth_ok do |user, ip|
332
+ log.info("successful authentication for #{user} from #{ip}")
333
+ end
334
+ end
335
+ ```
336
+
337
+ ## Shell Commands
338
+ If the pry-remote-em service is started with the ``:allow_shell_cmds =>
339
+ true`` option set it will spawn sub processes for any command prefixed
340
+ with a '.'.
341
+
342
+ ```
343
+ [1] pry(#<#<Class:0x007fe0be072618>>)> .uname -a
344
+ Darwin kiff.local 11.3.0 Darwin Kernel Version 11.3.0: Thu Jan 12 18:47:41 PST 2012; root:xnu-1699.24.23~1/RELEASE_X86_64 x86_64
345
+ ```
346
+
347
+ Interactive commands like ``vim`` will probably not behave
348
+ appropriately.
349
+
350
+
351
+ If the server was not started with the ``allow_shell_cmds`` option then
352
+ all shell commands will be met with a rejection notice.
353
+
354
+ ```
355
+ [1] pry(#<#<Class:0x007fe0be072618>>)> .ls
356
+ shell commands are not allowed by this server
357
+ ```
358
+
359
+ The server will also log whenever a user attempts to execute a shell
360
+ command.
361
+
362
+ ```
363
+ W, [2012-02-11T19:21:27.663941 #36471] WARN -- : executing shell command 'ls -al' for (127.0.0.1:63878)
364
+ ```
365
+
366
+ ```
367
+ E, [2012-02-11T19:23:40.770380 #36471] ERROR -- : refused to execute shell command 'ls' for caleb (127.0.0.1:63891)
368
+ ```
369
+
315
370
  # Missing Features
316
371
 
317
372
  - AutoDiscovery/Broker [ticket](https://github.com/simulacre/pry-remote-em/issues/11)
318
373
  - HTTP Transport [ticket](https://github.com/simulacre/pry-remote-em/issues/12)
319
- - Shell Commands [ticket](https://github.com/simulacre/pry-remote-em/issues/15)
320
374
  - Vi mode editing - RbReadline doesn't support vi edit mode. I'm looking into contributing it. PryRemoteEm uses rb-readline because the STLIB version doesn't play nice with Fibers.
321
375
  - Ssh key based authentication
322
376
 
@@ -18,8 +18,44 @@ end
18
18
 
19
19
 
20
20
  class Object
21
- def remote_pry_em(host = PryRemoteEm::DEFHOST, port = PryRemoteEm::DEFPORT, opts = {:tls => false})
21
+ def remote_pry_em(host = PryRemoteEm::DEFHOST, port = PryRemoteEm::DEFPORT, opts = {:tls => false}, &blk)
22
22
  opts = {:target => self}.merge(opts)
23
- PryRemoteEm::Server.run(opts[:target], host, port, opts)
23
+ PryRemoteEm::Server.run(opts[:target], host, port, opts, &blk)
24
24
  end
25
25
  end
26
+
27
+
28
+ unless defined?(EventMachine.popen3)
29
+ module EventMachine
30
+ # @see http://eventmachine.rubyforge.org/EventMachine.html#M000491
31
+ # @see https://gist.github.com/535644/4d5b645b96764e07ccb53539529bea9270741e1a
32
+ def self.popen3(cmd, handler=nil, *args)
33
+ klass = klass_from_handler(Connection, handler, *args)
34
+ w = Shellwords::shellwords(cmd)
35
+ w.unshift(w.first) if w.first
36
+
37
+ new_stderr = $stderr.dup
38
+ rd, wr = IO::pipe
39
+
40
+ $stderr.reopen wr
41
+ s = invoke_popen(w)
42
+ $stderr.reopen new_stderr
43
+
44
+ klass.new(s, *args).tap do |c|
45
+ EM.attach(rd, Popen3StderrHandler, c)
46
+ @conns[s] = c
47
+ yield(c) if block_given?
48
+ end
49
+ end
50
+
51
+ class Popen3StderrHandler < EventMachine::Connection
52
+ def initialize(connection)
53
+ @connection = connection
54
+ end
55
+
56
+ def receive_data(data)
57
+ @connection.receive_stderr(data)
58
+ end
59
+ end # class::Popen3StderrHandler
60
+ end # module::EventMachine
61
+ end # defined?(EventMachine.popen3)
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
  require 'pry-remote-em'
3
+ require 'pry-remote-em/client/keyboard'
3
4
  require 'pry/helpers/base_helpers'
4
5
  #require "readline" # doesn't work with Fiber.yield
5
6
  # - /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 @@ module PryRemoteEm
69
70
  elsif j['mb']
70
71
  Kernel.puts "\033[1m!! msg: " + j['mb'] + "\033[0m"
71
72
 
73
+ elsif j['s'] # shell command output
74
+ Kernel.puts j['s']
75
+
76
+ elsif j.include?('sc') # command completed
77
+ if @keyboard
78
+ @keyboard.bufferio(true)
79
+ @keyboard.close_connection
80
+ end
81
+
72
82
  elsif j['g'] # server banner
73
83
  Kernel.puts "[pry-remote-em] remote is #{j['g']}"
74
84
  name, version, scheme = j['g'].split(" ", 3)
@@ -133,6 +143,13 @@ module PryRemoteEm
133
143
  send_data({:b => l[2..-1]})
134
144
  elsif '!' == l[0]
135
145
  send_data({:m => l[1..-1]})
146
+ elsif '.' == l[0]
147
+ send_data({:s => l[1..-1]})
148
+ if Gem.loaded_specs["eventmachine"].version < Gem::Version.new("1.0.0.beta4")
149
+ Kernel.puts "\033[1minteractive shell commands are not well supported when running on EventMachine prior 1.0.0.beta4\033[0m"
150
+ else
151
+ @keyboard = EM.open_keyboard(Keyboard, self)
152
+ end
136
153
  else
137
154
  send_data(l)
138
155
  end # "!!" == l[0..1]
@@ -0,0 +1,46 @@
1
+ require "termios"
2
+ module PryRemoteEm
3
+ module Client
4
+ module Keyboard
5
+
6
+ def initialize(c)
7
+ @con = c
8
+ # TODO check actual current values to determine if it's enabled or not
9
+ @buff_enabled = true
10
+ # On EM < 1.0.0.beta.4 the keyboard handler and Termios don't work well together
11
+ # readline will complain that STDIN isn't a tty after Termios manipulation, so
12
+ # just don't let it happen
13
+ @manip_buff = Gem.loaded_specs["eventmachine"].version >= Gem::Version.new("1.0.0.beta.4")
14
+ bufferio(false)
15
+ # TODO retain the old SIGINT handler and reset it later
16
+ trap :SIGINT do
17
+ @con.send_data({:ssc => true})
18
+ end
19
+ end
20
+
21
+ def receive_data(d)
22
+ @con.send_data({:sd => d})
23
+ end
24
+
25
+ def unbind
26
+ bufferio(true)
27
+ trap :SIGINT do
28
+ Process.exit
29
+ end
30
+ end
31
+
32
+ # Makes stdin buffered or unbuffered.
33
+ # In unbuffered mode read and select will not wait for "\n"; also will not echo characters.
34
+ # This probably does not work on Windows.
35
+ # On EventMachine < 1.0.0.beta.4 this method doesn't do anything
36
+ def bufferio(enable)
37
+ return if !@manip_buff || (enable && @buff_enabled) || (!enable && !@buff_enabled)
38
+ attr = Termios.getattr($stdin)
39
+ enable ? (attr.c_lflag |= Termios::ICANON | Termios::ECHO) : (attr.c_lflag &= ~(Termios::ICANON|Termios::ECHO))
40
+ Termios.setattr($stdin, Termios::TCSANOW, attr)
41
+ @buff_enabled = enable
42
+ end
43
+ end # module::Keyboard
44
+ end # module::Client
45
+ end # module PryRemoteEm
46
+
@@ -1,5 +1,7 @@
1
1
  require 'pry'
2
+ require 'logger'
2
3
  require 'pry-remote-em'
4
+ require 'pry-remote-em/server/shell_cmd'
3
5
  # How it works with Pry
4
6
  #
5
7
  # When PryRemoteEm.run is called it registers with EventMachine for a given ip
@@ -34,6 +36,15 @@ module PryRemoteEm
34
36
  include JsonProto
35
37
 
36
38
  class << self
39
+ # Start a pry-remote-em server
40
+ # @param [Object] obj the object to bind pry to
41
+ # @param [String] ip the ip address to listen on
42
+ # @param [Fixnum, Symbol] port the port to listen on - if :auto the next available port will be taken
43
+ # @param [Hash] opts
44
+ # @option opts [Boolean] :tls require SSL encryption
45
+ # @option opts [Logger] :logger
46
+ # @option opts [Proc, Object] :auth require user authentication - see README
47
+ # @option opts [Boolean] :allow_shell_cmds
37
48
  def run(obj, host = DEFHOST, port = DEFPORT, opts = {:tls => false})
38
49
  tries = :auto == port ? 100.tap{ port = DEFPORT } : 1
39
50
  # TODO raise a useful exception not RuntimeError
@@ -42,6 +53,7 @@ module PryRemoteEm
42
53
  EM.start_server(host, port, PryRemoteEm::Server, obj, opts) do |pre|
43
54
  Fiber.new {
44
55
  begin
56
+ yield pre if block_given?
45
57
  Pry.start(obj, :input => pre, :output => pre)
46
58
  ensure
47
59
  pre.close_connection
@@ -58,7 +70,7 @@ module PryRemoteEm
58
70
  raise e
59
71
  end
60
72
  scheme = opts[:tls] ? 'pryems' : 'pryem'
61
- Kernel.puts "[pry-remote-em] listening for connections on #{scheme}://#{host}:#{port}/"
73
+ (opts[:logger] || ::Logger.new(STDERR)).info("[pry-remote-em] listening for connections on #{scheme}://#{host}:#{port}/")
62
74
  end # run(obj, host = DEFHOST, port = DEFPORT)
63
75
 
64
76
  # The list of pry-remote-em connections for a given object, or the list of all pry-remote-em
@@ -81,9 +93,27 @@ module PryRemoteEm
81
93
  end # class << self
82
94
 
83
95
  def initialize(obj, opts = {:tls => false})
84
- @obj = obj
85
- @after_auth = []
86
- @opts = opts
96
+ @obj = obj
97
+ @opts = opts
98
+ @allow_shell_cmds = opts[:allow_shell_cmds]
99
+ @log = opts[:logger] || ::Logger.new(STDERR)
100
+ # Blocks that will be called on each authentication attempt, prior checking the credentials
101
+ @auth_attempt_cbs = []
102
+ # All authentication attempts that occured before an auth callback was registered
103
+ @auth_attempts = []
104
+ # Blocks that will be called on each failed authentication attempt
105
+ @auth_fail_cbs = []
106
+ # All failed authentication attempts that occured before an auth callback was reigstered
107
+ @auth_fails = []
108
+ # Blocks that will be called on successful authentication attempt
109
+ @auth_ok_cbs = []
110
+ # All successful authentication attemps that occured before the auth ok callbacks were registered
111
+ @auth_oks = []
112
+
113
+ # The number maximum number of authentication attempts that are permitted
114
+ @auth_tries = 5
115
+ # Data to be sent after the user successfully authenticates if authentication is required
116
+ @after_auth = []
87
117
  return unless (a = opts[:auth])
88
118
  if a.is_a?(Proc)
89
119
  return fail("auth handler Procs must take two arguments not (#{a.arity})") unless a.arity == 2
@@ -95,28 +125,26 @@ module PryRemoteEm
95
125
  return error("auth handler objects must respond to :call, or :[]") unless a.respond_to?(:[])
96
126
  @auth = lambda {|u,p| a[u] && a[u] == p }
97
127
  end
98
- @auth_tries = 5
99
128
  end
100
129
 
101
130
  def post_init
102
131
  @lines = []
103
132
  Pry.config.pager, @old_pager = false, Pry.config.pager
104
- Pry.config.system, @old_system = PryRemoteEm::Server::System, Pry.config.system
105
133
  @auth_required = @auth
106
134
  port, ip = Socket.unpack_sockaddr_in(get_peername)
107
- Kernel.puts "[pry-remote-em] received client connection from #{ip}:#{port}"
135
+ @log.info("[pry-remote-em] received client connection from #{ip}:#{port}")
108
136
  send_data({:g => "PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}"})
109
137
  @opts[:tls] ? start_tls : (@auth_required && send_data({:a => false}))
110
138
  PryRemoteEm::Server.register(@obj, self)
111
139
  end
112
140
 
113
141
  def start_tls
114
- Kernel.puts "[pry-remote-em] starting TLS (#{peer_ip}:#{peer_port})"
142
+ @log.debug("[pry-remote-em] starting TLS (#{peer_ip}:#{peer_port})")
115
143
  super(@opts[:tls].is_a?(Hash) ? @opts[:tls] : {})
116
144
  end
117
145
 
118
146
  def ssl_handshake_completed
119
- Kernel.puts "[pry-remote-em] TLS connection established (#{peer_ip}:#{peer_port})"
147
+ @log.info("[pry-remote-em] TLS connection established (#{peer_ip}:#{peer_port})")
120
148
  send_data({:a => false}) if @auth_required
121
149
  end
122
150
 
@@ -150,13 +178,17 @@ module PryRemoteEm
150
178
  elsif j['a'] # authentication response
151
179
  return send_data({:a => true}) if !@auth || !@auth_required
152
180
  return send_data({:a => 'auth data must be a two element array'}) unless j['a'].is_a?(Array) && j['a'].length == 2
181
+ auth_attempt(j['a'][0], peer_ip)
153
182
  unless (@auth_required = !@auth.call(*j['a']))
183
+ @user = j['a'][0]
184
+ auth_ok(j['a'][0], peer_ip)
154
185
  authenticated!
155
186
  else
187
+ auth_fail(j['a'][0], peer_ip)
156
188
  if @auth_tries <= 0
157
189
  msg = "max authentication attempts reached"
158
190
  send_data({:a => msg})
159
- Kernel.puts "[pry-remote-em] #{msg} (#{peer_ip}:#{peer_port})"
191
+ @log.debug("[pry-remote-em] #{msg} (#{peer_ip}:#{peer_port})")
160
192
  return close_connection_after_writing
161
193
  end
162
194
  @auth_tries -= 1
@@ -171,6 +203,24 @@ module PryRemoteEm
171
203
  peers(:all).each { |peer| peer.send_bmessage(j['b']) }
172
204
  send_last_prompt
173
205
 
206
+ elsif j['s'] # shell command
207
+ # TODO confirm shell command's are allowed
208
+ unless @allow_shell_cmds
209
+ puts "\033[1mshell commands are not allowed by this server\033[0m"
210
+ @log.error("refused to execute shell command '#{j['s']}' for #{@user} (#{peer_ip}:#{peer_port})")
211
+ send_data({:sc => -1})
212
+ send_last_prompt
213
+ else
214
+ @log.warn("executing shell command '#{j['s']}' for #{@user} (#{peer_ip}:#{peer_port})")
215
+ @shell_cmd = EM.popen3(j['s'], ShellCmd, self)
216
+ end
217
+
218
+ elsif j['sd'] # shell data
219
+ @shell_cmd.send_data(j['sd'])
220
+
221
+ elsif j['ssc'] # shell ctrl-c
222
+ @shell_cmd.close_connection
223
+
174
224
  else
175
225
  warn "received unexpected data: #{j.inspect}"
176
226
  end # j['d']
@@ -185,9 +235,8 @@ module PryRemoteEm
185
235
 
186
236
  def unbind
187
237
  Pry.config.pager = @old_pager
188
- Pry.config.system = @old_system
189
238
  PryRemoteEm::Server.unregister(@obj, self)
190
- Kernel.puts "[pry-remote-em] remote session terminated (#{peer_ip}:#{peer_port})"
239
+ @log.debug("[pry-remote-em] remote session terminated (#{peer_ip}:#{peer_port})")
191
240
  end
192
241
 
193
242
  def peers(all = false)
@@ -210,6 +259,45 @@ module PryRemoteEm
210
259
  @auth_required ? @after_auth.push({:mb => msg}) : send_data({:mb => msg})
211
260
  end
212
261
 
262
+ # Callbacks for events on the server
263
+
264
+ # Registers a block to call when authentication is attempted.
265
+ # @overload auth_attempt(&blk)
266
+ # @yield [user, ip] a block to call on each authentication attempt
267
+ # @yieldparam [String] user
268
+ # @yieldparam [String] ip
269
+ def auth_attempt(*args, &blk)
270
+ block_given? ? @auth_attempt_cbs << blk : @auth_attempts.push(args)
271
+ while (auth_data = @auth_attempts.shift)
272
+ @auth_attempt_cbs.each { |cb| cb.call(*auth_data) }
273
+ end
274
+ end # auth_attempt(*args, &blk)
275
+
276
+ # Registers a block to call when authentication fails.
277
+ # @overload auth_fail(&blk)
278
+ # @yield [user, ip] a block to call after each failed authentication attempt
279
+ # @yieldparam [String] user
280
+ # @yieldparam [String] ip
281
+ def auth_fail(*args, &blk)
282
+ block_given? ? @auth_fail_cbs << blk : @auth_fails.push(args)
283
+ while (fail_data = @auth_fails.shift)
284
+ @auth_fail_cbs.each { |cb| cb.call(*fail_data) }
285
+ end
286
+ end # auth_fail(*args, &blk)
287
+
288
+ # Registers a block to call when authentication succeeds.
289
+ # @overload auth_ok(&blk)
290
+ # @yield [user, ip] a block to call after each successful authentication attempt
291
+ # @yieldparam [String] user
292
+ # @yieldparam [String] ip
293
+ def auth_ok(*args, &blk)
294
+ block_given? ? @auth_ok_cbs << blk : @auth_oks.push(args)
295
+ while (ok_data = @auth_oks.shift)
296
+ @auth_ok_cbs.each { |cb| cb.call(*ok_data) }
297
+ end
298
+ end # auth_fail(*args, &blk)
299
+
300
+
213
301
  # Methods that make Server compatible with Pry
214
302
 
215
303
  def readline(prompt)
@@ -241,9 +329,5 @@ module PryRemoteEm
241
329
  def flush
242
330
  true
243
331
  end
244
-
245
- System = proc do |output, cmd, _|
246
- output.puts("shell commands are not yet supported")
247
- end
248
332
  end # module::Server
249
333
  end # module::PryRemoteEm
@@ -0,0 +1,20 @@
1
+ module PryRemoteEm
2
+ module Server
3
+ module ShellCmd
4
+ def initialize(pryem)
5
+ @pryem = pryem
6
+ end
7
+
8
+ def receive_data(d)
9
+ @pryem.send_data({:s => d.force_encoding("utf-8") })
10
+ end
11
+
12
+ alias :receive_stderr :receive_data
13
+
14
+ def unbind
15
+ @pryem.send_data({:sc => get_status.exitstatus })
16
+ @pryem.send_last_prompt
17
+ end
18
+ end # module::ShellCmd
19
+ end # module::Server
20
+ end # module::PryRemoteEm
@@ -1,3 +1,3 @@
1
1
  module PryRemoteEm
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pry-remote-em
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-06 00:00:00.000000000Z
12
+ date: 2012-02-11 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
16
- requirement: &70256102479660 !ruby/object:Gem::Requirement
16
+ requirement: &70351121077580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70256102479660
24
+ version_requirements: *70351121077580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: pry
27
- requirement: &70256102473680 !ruby/object:Gem::Requirement
27
+ requirement: &70351121077080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.9.6
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70256102473680
35
+ version_requirements: *70351121077080
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rb-readline
38
- requirement: &70256102473100 !ruby/object:Gem::Requirement
38
+ requirement: &70351121076660 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,21 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70256102473100
46
+ version_requirements: *70351121076660
47
+ - !ruby/object:Gem::Dependency
48
+ name: ruby-termios
49
+ requirement: &70351121076120 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.9.6
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70351121076120
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: highline
49
- requirement: &70256102471840 !ruby/object:Gem::Requirement
60
+ requirement: &70351121109480 !ruby/object:Gem::Requirement
50
61
  none: false
51
62
  requirements:
52
63
  - - ! '>='
@@ -54,7 +65,7 @@ dependencies:
54
65
  version: '0'
55
66
  type: :runtime
56
67
  prerelease: false
57
- version_requirements: *70256102471840
68
+ version_requirements: *70351121109480
58
69
  description: Connect to Pry remotely using EventMachine with tab-completion, paging,
59
70
  user auth and SSL
60
71
  email: pry-remote-em@simulacre.org
@@ -63,8 +74,10 @@ executables:
63
74
  extensions: []
64
75
  extra_rdoc_files: []
65
76
  files:
77
+ - lib/pry-remote-em/client/keyboard.rb
66
78
  - lib/pry-remote-em/client.rb
67
79
  - lib/pry-remote-em/json-proto.rb
80
+ - lib/pry-remote-em/server/shell_cmd.rb
68
81
  - lib/pry-remote-em/server.rb
69
82
  - lib/pry-remote-em/version.rb
70
83
  - lib/pry-remote-em.rb
@@ -95,4 +108,3 @@ signing_key:
95
108
  specification_version: 3
96
109
  summary: Connect to Pry remotely using EventMachine
97
110
  test_files: []
98
- has_rdoc: