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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Proto

#receive_banner, #receive_data, #receive_object, #receive_prompt, #receive_proxy_connection, #receive_register_server, #receive_server_list, #receive_server_reload_list, #receive_shell_result, #receive_start_tls, #receive_unregister_server, #send_auth, #send_banner, #send_clear_buffer, #send_completion, #send_heatbeat, #send_msg, #send_msg_bcast, #send_object, #send_prompt, #send_proxy_connection, #send_raw, #send_register_server, #send_server_list, #send_server_reload_list, #send_shell_cmd, #send_shell_data, #send_shell_result, #send_shell_sig, #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.



115
116
117
118
# File 'lib/pry-remote-em/server.rb', line 115

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.



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

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

.run(options = {}, &block) ⇒ Object

Start a pry-remote-em server

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :target (Object)

    Object to bind Pry session, default - PryRemoteEm::Sandbox instance

  • :host (String)

    The IP-address to listen on, default - 127.0.0.1 (same as PRYEMHOST environment variable)

  • :port (Fixnum, String, Symbol)

    The port to listen on - if :auto or ‘auto’ the next available port will be taken, default - 6463 (same as PRYEMPORT environment variable)

  • :id (String)

    Server’s UUID, will be generated automatically unless you pass it explicitly

  • :tls (Boolean)

    require SSL encryption, default - false

  • :logger (Logger)

    Logger for Pry Server, default - STDERR

  • :auth (Proc, Object)

    require user authentication - see README

  • :allow_shell_cmds (Boolean)

    Allow shell commands or not, default - true

  • :port_fail (Integer, Symbol)

    set to :auto to search for available port in range from given port to port + 100, or pass explicit integer to use instaed of 100, default - 1

  • :name (String)

    Server name to show in broker list, default - object’s inspect (same as PRYEMNAME environment variable)

  • :external_url (String)

    External URL to connect behind firewall, NAT, Docket etc. in form “pryem://my.host:6463”, default - use given host and port and expand it to all interfaces in case of 0.0.0.0 (same as PRYEMURL environment variable)

  • :heartbeat_interval (Integer)

    Interval to send heartbeats and updated details to broker, default - 15 (same as PRYEMHBSEND environment variable)

  • :remote_broker (Boolean)

    Connect to remote broker instead of starting local one, default - false (same as PRYEMREMOTEBROKER environment variable)

  • :broker_host (String)

    Broker host to connect to, default - localhost

  • :broker_port (String)

    Broker port to connect to, default - 6462

  • :details (Hash)

    Optional details to pass to broker and show in table (should consist of string/symbol keys and simple serializable values)



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pry-remote-em/server.rb', line 92

def run(options = {}, &block)
  description = options.dup
  description[:target] ||= PryRemoteEm::Sandbox.new
  description[:host] ||= ENV['PRYEMHOST'].nil? || ENV['PRYEMHOST'].empty? ? DEFAULT_SERVER_HOST : ENV['PRYEMHOST']
  determine_port_and_tries(description)
  determine_name(description)
  description[:id] ||= SecureRandom.uuid
  description[:logger] ||= ::Logger.new(STDERR)
  description[:external_url] ||= ENV['PRYEMURL'] || "#{description[:tls] ? 'pryems' : 'pryem'}://#{description[:host]}:#{description[:port]}/"
  description[:details] ||= {}
  description[:allow_shell_cmds] = true if description[:allow_shell_cmds].nil?
  description[:heartbeat_interval] ||= ENV['PRYEMHBSEND'].nil? || ENV['PRYEMHBSEND'].empty? ? HEARTBEAT_SEND_INTERVAL : ENV['PRYEMHBSEND']
  description[:urls] = expand_url(description[:external_url])
  description[:server] = start_server(description, &block)
  description[:logger].info("[pry-remote-em] listening for connections on #{description[:external_url]}")
  PryRemoteEm.servers[description[:id]] = description
  register_in_broker(description)
  return description
end

.unregister(obj, peer) ⇒ Object

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



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

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

Instance Method Details

#after_auth(&blk) ⇒ Object



424
425
426
427
# File 'lib/pry-remote-em/server.rb', line 424

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)


448
449
450
451
452
453
# File 'lib/pry-remote-em/server.rb', line 448

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)


460
461
462
463
464
465
# File 'lib/pry-remote-em/server.rb', line 460

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)


472
473
474
475
476
477
# File 'lib/pry-remote-em/server.rb', line 472

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



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

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

#completion_proc=(compl) ⇒ Object



503
504
505
# File 'lib/pry-remote-em/server.rb', line 503

def completion_proc=(compl)
  @compl_proc = compl
end

#flushObject



511
512
513
# File 'lib/pry-remote-em/server.rb', line 511

def flush
  true
end

#initialize(opts = {}) ⇒ Object

class << self



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/pry-remote-em/server.rb', line 218

def initialize(opts = {})
  @obj              = opts[:target]
  @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



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

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



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

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



414
415
416
417
418
# File 'lib/pry-remote-em/server.rb', line 414

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

#post_initObject



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/pry-remote-em/server.rb', line 258

def post_init
  @lines = []
  @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'}")
  need_new_line = false
  if @opts[:details].any?
    send_raw("\nServer details:\n#{@opts[:details].map { |key, value| "  #{key}: #{value}" } * "\n"}\n")
    need_new_line = true
  end
  if PryRemoteEm::Metrics.any?
    send_raw("\nServer metrics:\n#{PryRemoteEm::Metrics.list.map { |key, value| "  #{key}: #{value}" } * "\n"}\n")
    need_new_line = true
  end
  send_raw("\n") if need_new_line
  @log.info("#{url} PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}")
  @opts[:tls] ? start_tls : (@auth_required && send_auth(false))
  PryRemoteEm::Server.register(@obj, self)
end


493
494
495
# File 'lib/pry-remote-em/server.rb', line 493

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

#puts(data = '') ⇒ Object



498
499
500
501
# File 'lib/pry-remote-em/server.rb', line 498

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



485
486
487
488
489
490
491
# File 'lib/pry-remote-em/server.rb', line 485

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



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/pry-remote-em/server.rb', line 334

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_clear_bufferObject



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

def receive_clear_buffer
  @opts[:pry].eval_string.replace('')
  @last_prompt = @opts[:pry].select_prompt
  send_last_prompt
end

#receive_completion(c) ⇒ Object

tab completion request



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

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

#receive_msg(m) ⇒ Object



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

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



361
362
363
364
365
# File 'lib/pry-remote-em/server.rb', line 361

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



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/pry-remote-em/server.rb', line 310

def receive_raw(d)
  return if require_auth

  return send_last_prompt if d.nil?

  if d.empty?
    @lines.push('')
  else
    lines = d.split("\n")
    @lines.push(*lines)
  end

  if @waiting
    f, @waiting = @waiting, nil
    f.resume(@lines.shift)
  end
end

#receive_shell_cmd(cmd) ⇒ Object



367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/pry-remote-em/server.rb', line 367

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



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

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

#receive_shell_sig(type) ⇒ Object



385
386
387
388
# File 'lib/pry-remote-em/server.rb', line 385

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

#receive_unknown(j) ⇒ Object



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

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



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

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.



436
437
438
439
# File 'lib/pry-remote-em/server.rb', line 436

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)



479
480
481
# File 'lib/pry-remote-em/server.rb', line 479

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

#send_last_promptObject



420
421
422
# File 'lib/pry-remote-em/server.rb', line 420

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.



430
431
432
433
# File 'lib/pry-remote-em/server.rb', line 430

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



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

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



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

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)


507
508
509
# File 'lib/pry-remote-em/server.rb', line 507

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

#unbindObject



409
410
411
412
# File 'lib/pry-remote-em/server.rb', line 409

def unbind
  PryRemoteEm::Server.unregister(@obj, self)
  @log.debug("[pry-remote-em] remote session terminated (#{@ip}:#{@port})")
end

#urlObject



253
254
255
256
# File 'lib/pry-remote-em/server.rb', line 253

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