Module: PryRemoteEm::Server

Includes:
Proto
Defined in:
lib/pry-remote-em/server.rb,
lib/pry-remote-em/server/shell_cmd.rb

Defined Under Namespace

Modules: ShellCmd

Constant Summary

Constants included from Proto

Proto::PREAMBLE, Proto::PREAMBLE_LEN, Proto::SEPERATOR, Proto::SEPERATOR_LEN

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Proto

#receive_banner, #receive_data, #receive_json, #receive_prompt, #receive_proxy_connection, #receive_register_server, #receive_server_list, #receive_shell_result, #receive_start_tls, #receive_unregister_server, #send_auth, #send_banner, #send_completion, #send_data, #send_heatbeat, #send_json, #send_msg, #send_msg_bcast, #send_prompt, #send_proxy_connection, #send_raw, #send_register_server, #send_server_list, #send_shell_cmd, #send_shell_result, #send_start_tls, #send_unregister_server

Class Method Details

.peers(obj = nil) ⇒ Object

The list of pry-remote-em connections for a given object, or the list of all pry-remote-em connections for this process. The peer list is used when broadcasting messages between connections.



119
120
121
122
# File 'lib/pry-remote-em/server.rb', line 119

def peers(obj = nil)
  @peers ||= {}
  obj.nil? ? @peers.values.flatten : (@peers[obj] ||= [])
end

.register(obj, peer) ⇒ Object

Record the association between a given object and a given pry-remote-em connection.



125
126
127
# File 'lib/pry-remote-em/server.rb', line 125

def register(obj, peer)
  peers(obj).tap { |plist| plist.include?(peer) || plist.push(peer) }
end

.run(obj, host = DEFHOST, port = DEFPORT, opts = {:tls => false}) ⇒ Object

Start a pry-remote-em server

Parameters:

  • obj (Object)

    the object to bind pry to

  • ip (String)

    the ip address to listen on

  • port (Fixnum, Symbol) (defaults to: DEFPORT)

    the port to listen on - if :auto the next available port will be taken

  • opts (Hash) (defaults to: {:tls => false})

Options Hash (opts):

  • :tls (Boolean)

    require SSL encryption

  • :logger (Logger)
  • :auth (Proc, Object)

    require user authentication - see README

  • :allow_shell_cmds (Boolean)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/pry-remote-em/server.rb', line 77

def run(obj, host = DEFHOST, port = DEFPORT, opts = {:tls => false})
  tries = [port, opts[:port_fail]].include?(:auto) ? 100 : 1
  port  = DEFPORT if :auto == port
  # TODO raise a useful exception not RuntimeError
  raise "root permission required for port below 1024 (#{port})" if port < 1024 && Process.euid != 0
  begin
    server = EM.start_server(host, port, PryRemoteEm::Server, obj, opts) do |pre|
      Fiber.new {
        begin
          yield pre if block_given?
          Pry.start(obj, :input => pre, :output => pre)
        ensure
          pre.close_connection
        end
      }.resume
    end
  rescue => e
    # EM 1.0.0.beta4's message tells us the port is in use; 0.12.10 just says, 'no acceptor'
    if (e.message.include?('port is in use') || e.message.include?('no acceptor')) && tries > 1
      tries -= 1
      port += 1
      retry
    end
    raise "can't bind to #{host}:#{port} - #{e}"
  end
  url    = "#{opts[:tls] ? 'pryems' : 'pryem'}://#{host}:#{port}/"
  name   = obj.send(:eval, 'self') rescue "#{obj}"
  name   = Pry.view_clip(name)
  PryRemoteEm.servers[url] = [server, name]
  (opts[:logger] || ::Logger.new(STDERR)).info("[pry-remote-em] listening for connections on #{url}")
  Broker.run(opts[:broker_host] || ENV['PRYEMBROKER'] || DEF_BROKERHOST, opts[:broker_port] || ENV['PRYEMBROKERPORT'] || DEF_BROKERPORT, opts) do |broker|
    broker.register(url, name)
    rereg = EM::PeriodicTimer.new(15) do
      EM.get_sockname(server) ? broker.register(url, name) : nil #rereg.cancel
    end
  end # broker
  url
end

.unregister(obj, peer) ⇒ Object

Remove the association between a given object and a given pry-remote-em connection.



130
131
132
# File 'lib/pry-remote-em/server.rb', line 130

def unregister(obj, peer)
  peers(obj).tap {|plist| true while plist.delete(peer) }
end

Instance Method Details

#after_auth(&blk) ⇒ Object



319
320
321
322
# File 'lib/pry-remote-em/server.rb', line 319

def after_auth(&blk)
  # TODO perhaps replace with #auth_ok
  @after_auth.push(blk)
end

#auth_attempt {|user, ip| ... } ⇒ Object

Registers a block to call when authentication is attempted.

Yields:

  • (user, ip)

    a block to call on each authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


343
344
345
346
347
348
# File 'lib/pry-remote-em/server.rb', line 343

def auth_attempt(*args, &blk)
  block_given? ? @auth_attempt_cbs << blk : @auth_attempts.push(args)
  while (auth_data = @auth_attempts.shift)
    @auth_attempt_cbs.each { |cb| cb.call(*auth_data) }
  end
end

#auth_fail {|user, ip| ... } ⇒ Object

Registers a block to call when authentication fails.

Yields:

  • (user, ip)

    a block to call after each failed authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


355
356
357
358
359
360
# File 'lib/pry-remote-em/server.rb', line 355

def auth_fail(*args, &blk)
  block_given? ? @auth_fail_cbs << blk : @auth_fails.push(args)
  while (fail_data = @auth_fails.shift)
    @auth_fail_cbs.each { |cb| cb.call(*fail_data) }
  end
end

#auth_ok {|user, ip| ... } ⇒ Object

Registers a block to call when authentication succeeds.

Yields:

  • (user, ip)

    a block to call after each successful authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


367
368
369
370
371
372
# File 'lib/pry-remote-em/server.rb', line 367

def auth_ok(*args, &blk)
  block_given? ? @auth_ok_cbs << blk : @auth_oks.push(args)
  while (ok_data = @auth_oks.shift)
    @auth_ok_cbs.each { |cb| cb.call(*ok_data) }
  end
end

#authenticated!Object



297
298
299
300
301
# File 'lib/pry-remote-em/server.rb', line 297

def authenticated!
  while (aa = @after_auth.shift)
    aa.call
  end
end

#completion_proc=(compl) ⇒ Object



398
399
400
# File 'lib/pry-remote-em/server.rb', line 398

def completion_proc=(compl)
  @compl_proc = compl
end

#flushObject



406
407
408
# File 'lib/pry-remote-em/server.rb', line 406

def flush
  true
end

#initialize(obj, opts = {:tls => false}) ⇒ Object

class << self



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/pry-remote-em/server.rb', line 135

def initialize(obj, opts = {:tls => false})
  @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
  @auth_attempts    = []
  # Blocks that will be called on each failed authentication attempt
  @auth_fail_cbs    = []
  # All failed authentication attempts that occured before an auth callback was reigstered
  @auth_fails       = []
  # Blocks that will be called on successful authentication attempt
  @auth_ok_cbs      = []
  # All successful authentication attemps that occured before the auth ok callbacks were registered
  @auth_oks         = []

  # The number maximum number of authentication attempts that are permitted
  @auth_tries       = 5
  # Data to be sent after the user successfully authenticates if authentication is required
  @after_auth       = []
  return unless (a = opts[:auth])
  if a.is_a?(Proc)
    return fail("auth handler Procs must take two arguments not (#{a.arity})") unless a.arity == 2
    @auth = a
  elsif a.respond_to?(:call)
    return fail("auth handler must take two arguments not (#{a.method(:call).arity})") unless a.method(:call).arity == 2
    @auth = a
  else
    return error("auth handler objects must respond to :call, or :[]") unless a.respond_to?(:[])
    @auth = lambda {|u,p| a[u] && a[u] == p }
  end
end

#peer_ipObject



198
199
200
201
202
203
# File 'lib/pry-remote-em/server.rb', line 198

def peer_ip
  return @peer_ip if @peer_ip
  return "" if get_peername.nil?
  @peer_port, @peer_ip = Socket.unpack_sockaddr_in(get_peername)
  @peer_ip
end

#peer_portObject



205
206
207
208
209
210
# File 'lib/pry-remote-em/server.rb', line 205

def peer_port
  return @peer_port if @peer_port
  return "" if get_peername.nil?
  @peer_port, @peer_ip = Socket.unpack_sockaddr_in(get_peername)
  @peer_port
end

#peers(all = false) ⇒ Object



309
310
311
312
313
# File 'lib/pry-remote-em/server.rb', line 309

def peers(all = false)
  plist = (all ? PryRemoteEm::Server.peers : PryRemoteEm::Server.peers(@obj)).clone
  plist.delete(self)
  plist
end

#post_initObject



175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/pry-remote-em/server.rb', line 175

def post_init
  @lines = []
  Pry.config.pager, @old_pager = false, Pry.config.pager
  @auth_required  = @auth
  port, ip        = Socket.unpack_sockaddr_in(get_peername)
  @log.info("[pry-remote-em] received client connection from #{ip}:#{port}")
  # TODO include first level prompt in banner
  send_banner("PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}")
  @log.info("#{url} PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}")
  @opts[:tls] ? start_tls : (@auth_required && send_auth(false))
  PryRemoteEm::Server.register(@obj, self)
end


388
389
390
# File 'lib/pry-remote-em/server.rb', line 388

def print(val)
  @auth_required ? (after_auth { send_raw(val) }) : send_raw(val)
end

#puts(data = "") ⇒ Object



393
394
395
396
# File 'lib/pry-remote-em/server.rb', line 393

def puts(data = "")
  s = data.to_s
  print(s[0] == "\n" ? s : s + "\n")
end

#readline(prompt) ⇒ Object

Methods that make Server compatible with Pry



380
381
382
383
384
385
386
# File 'lib/pry-remote-em/server.rb', line 380

def readline(prompt)
  @last_prompt = prompt
  @auth_required ? (after_auth { send_prompt(prompt) }) : send_prompt(prompt)
  return @lines.shift unless @lines.empty?
  @waiting = Fiber.current
  return Fiber.yield
end

#receive_auth(user, pass) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/pry-remote-em/server.rb', line 228

def receive_auth(user, pass)
  return send_auth(true) if !@auth || !@auth_required
  return send_auth('auth data must include a user and pass') if user.nil? || pass.nil?
  auth_attempt(user, peer_ip)
  unless (@auth_required = !@auth.call(user, pass))
    @user = user
    auth_ok(user, peer_ip)
    authenticated!
  else
   auth_fail(user, peer_ip)
    if @auth_tries <= 0
      msg = "max authentication attempts reached"
      send_auth(msg)
      @log.debug("[pry-remote-em] #{msg} (#{peer_ip}:#{peer_port})")
      return close_connection_after_writing
    end
    @auth_tries -= 1
  end
  return send_auth(!@auth_required)
end

#receive_completion(c) ⇒ Object

tab completion request



223
224
225
226
# File 'lib/pry-remote-em/server.rb', line 223

def receive_completion(c)
  return if require_auth
  send_completion(@compl_proc.call(c))
end

#receive_msg(m) ⇒ Object



249
250
251
252
253
# File 'lib/pry-remote-em/server.rb', line 249

def receive_msg(m)
  return if require_auth
  peers.each { |peer| peer.send_message(m, @user) }
  send_last_prompt
end

#receive_msg_bcast(mb) ⇒ Object



255
256
257
258
259
# File 'lib/pry-remote-em/server.rb', line 255

def receive_msg_bcast(mb)
  return if require_auth
  peers(:all).each { |peer| peer.send_bmessage(mb, @user) }
  send_last_prompt
end

#receive_raw(d) ⇒ Object



212
213
214
215
216
217
218
219
220
# File 'lib/pry-remote-em/server.rb', line 212

def receive_raw(d)
  return if require_auth
  return send_last_prompt if d.nil? || d.empty?
  @lines.push(*d.split("\n"))
  if @waiting
    f, @waiting = @waiting, nil
    f.resume(@lines.shift)
  end
end

#receive_shell_cmd(cmd) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/pry-remote-em/server.rb', line 261

def receive_shell_cmd(cmd)
  return if require_auth
  unless @allow_shell_cmds
    puts "\033[1mshell commands are not allowed by this server\033[0m"
    @log.error("refused to execute shell command '#{cmd}' for #{@user} (#{peer_ip}:#{peer_port})")
    send_shell_result(-1)
    send_last_prompt
  else
    @log.warn("executing shell command '#{cmd}' for #{@user} (#{peer_ip}:#{peer_port})")
    @shell_cmd = EM.popen3(cmd, ShellCmd, self)
  end
end

#receive_shell_data(d) ⇒ Object



274
275
276
277
# File 'lib/pry-remote-em/server.rb', line 274

def receive_shell_data(d)
  return if require_auth
  @shell_cmd.send_data(d)
end

#receive_shell_sig(type) ⇒ Object



279
280
281
282
# File 'lib/pry-remote-em/server.rb', line 279

def receive_shell_sig(type)
  return if require_auth
  type == :term && @shell_cmd.close_connection
end

#receive_unknown(j) ⇒ Object



284
285
286
287
288
289
# File 'lib/pry-remote-em/server.rb', line 284

def receive_unknown(j)
  return if require_auth
  warn "received unexpected data: #{j.inspect}"
  send_error("received unexpected data: #{j.inspect}")
  send_last_prompt
end

#require_authObject



291
292
293
294
295
# File 'lib/pry-remote-em/server.rb', line 291

def require_auth
  return false if !@auth_required
  send_auth(false)
  true
end

#send_bmessage(msg, from = nil) ⇒ Object

Sends a chat message to the client.



331
332
333
334
# File 'lib/pry-remote-em/server.rb', line 331

def send_bmessage(msg, from = nil)
  msg = "#{msg} (@#{from})" unless from.nil?
  @auth_required ?  (after_auth {send_msg_bcast(msg)}) : send_msg_bcast(msg)
end

#send_error(msg) ⇒ Object

auth_fail(*args, &blk)



374
375
376
# File 'lib/pry-remote-em/server.rb', line 374

def send_error(msg)
  puts "\033[31m#{msg}\033[0m"
end

#send_last_promptObject



315
316
317
# File 'lib/pry-remote-em/server.rb', line 315

def send_last_prompt
  @auth_required ? (after_auth { send_prompt(@last_prompt) }) :  send_prompt(@last_prompt)
end

#send_message(msg, from = nil) ⇒ Object

Sends a chat message to the client.



325
326
327
328
# File 'lib/pry-remote-em/server.rb', line 325

def send_message(msg, from = nil)
  msg = "#{msg} (@#{from})" unless from.nil?
  @auth_required ?  (after_auth {send_msg(msg)}) : send_msg(msg)
end

#ssl_handshake_completedObject



193
194
195
196
# File 'lib/pry-remote-em/server.rb', line 193

def ssl_handshake_completed
  @log.info("[pry-remote-em] TLS connection established (#{peer_ip}:#{peer_port})")
  send_auth(false) if @auth_required
end

#start_tlsObject



188
189
190
191
# File 'lib/pry-remote-em/server.rb', line 188

def start_tls
  @log.debug("[pry-remote-em] starting TLS (#{peer_ip}:#{peer_port})")
  super(@opts[:tls].is_a?(Hash) ? @opts[:tls] : {})
end

#tty?Boolean

Returns:

  • (Boolean)


402
403
404
# File 'lib/pry-remote-em/server.rb', line 402

def tty?
  true # might be a very bad idea ....
end

#unbindObject



303
304
305
306
307
# File 'lib/pry-remote-em/server.rb', line 303

def unbind
  Pry.config.pager  = @old_pager
  PryRemoteEm::Server.unregister(@obj, self)
  @log.debug("[pry-remote-em] remote session terminated (#{peer_ip}:#{peer_port})")
end

#urlObject



170
171
172
173
# File 'lib/pry-remote-em/server.rb', line 170

def url
  port, host = Socket.unpack_sockaddr_in(get_sockname)
  "#{@opts[:tls] ? 'pryems' : 'pryem'}://#{host}:#{port}/"
end